Skip to content

feat: generate ScanAPI spec from OpenAPI#862

Draft
guites wants to merge 19 commits intoscanapi:mainfrom
guites:scanapi_spec_from_openapi_v2
Draft

feat: generate ScanAPI spec from OpenAPI#862
guites wants to merge 19 commits intoscanapi:mainfrom
guites:scanapi_spec_from_openapi_v2

Conversation

@guites
Copy link
Contributor

@guites guites commented Feb 1, 2026

Description

This is a draft implementation for creating a "Skeleton" ScanAPI test suite from OpenAPI v3 spec files (.json or .yaml).

Please note that the code is not yet production ready, as I wanted to draft a solution in order to advance discussion.

Motivation behind this PR?

Increasing ScanAPI adoption by reducing friction is a long going issue (see #814).

This PR attempts to provide a sample script that converts OpenAPI v3 files, which are very popular, into a scanapi.yaml file.

The objective is to give project maintainers a starting point into implementation ScanAPI to their pipelines.

I'm referring to this starting scanapi.yaml file as a skeleton file. The skeleton should include (by running the convertion script once):

  • One request for each existing endpoint listed on the OpenAPI spec.
  • Each request should have a simple test that checks whether the expected HTTP status for that endpoint is being returned.
  • Each request (that expects a path variable in the URL) should have the URL pre populated with a custom variable.
  • Each request (that expects an HTTP body) should have the body pre populated with custom variables.
  • Each request (that expects authentication) should have the "headers" section pre populated with custom variables.

Benefits to this approach:

  1. An adopting project can quickly set up a custom scanapi.yaml file;
  2. The adopting project immediately benefits by receiving a set of automated "sanity tests" (with minimal tinkering)

Some downsides:

  1. After running the convertion script, the project maintainer will be expected to fill missing information such as creating custom variables and/or environment variables. I don't think this is a huge issue because it's less work than starting from scratch
  2. After generating the skeleton file, any changes to it would be lost if the convertion script is ran again. This could be prevented by implementing a "sync" mechanism, where additions or removals from the OpenAPI spec file are reflected on the scanapi.yaml file. I think this is out of the scope of the current implementation.
  3. This adds a few dependencies (namely, two) to our project: prance (https://github.com/RonnyPfannschmidt/prance) and openapi-spec-validator (https://github.com/python-openapi/openapi-spec-validator).

Example usage

Let's take the Futurama API project as an example. It's swagger documentation can be accessed here: https://futuramaapi.com/swagger#/ .

We can download the OpenAPI spec file (from https://futuramaapi.com/openapi.json) and run the following command:

$ uv run scanapi convert openapi-futurama-api.json -o scanapi-futurama.yaml
OpenAPI/Swagger version detected: 3.1.0

The following variables were created in the generated ScanAPI YAML file:
- ${Create_User_surname}
- ${Character_Sse_character_id}
- ${Get_Link_link_id}
- ${Create_User_username}
- ${Create_Favorite_Character_character_id}
- ${Create_User_name}
- ${Create_User_password}
- ${Get_User_Auth_Token_username}
- ${Episode_Callback_episode_id}
- ${Character_Callback_callbackUrl}
- ${Get_User_Auth_Token_password}
- ${Create_User_email}
- ${Get_Secret_Message_url}
- ${bearer_token}
- ${Create_Secret_Message_text}
- ${Character_Callback_character_id}
- ${Season_Callback_callbackUrl}
- ${Episode_episode_id}
- ${Character_character_id}
- ${Delete_Favorite_Character_character_id}
- ${Request_Change_User_Password_email}
- ${Season_season_id}
- ${Create_Link_url}
- ${Season_Callback_season_id}
- ${Episode_Callback_callbackUrl}
- ${Get_Refreshed_User_Auth_Token_refresh_token}
See https://scanapi.dev/docs_v1/specification/custom_variables and https://scanapi.dev/docs_v1/specification/environment_variables for more information.

File successfully converted and exported as "scanapi-futurama.yaml"!

This would result in the following ScanAPI yaml:

$ cat scanapi-futurama.yaml

endpoints:
-   name: FastAPI
    path: ${BASE_URL}
    requests:
    -   name: Character_Callback
        path: /api/callbacks/characters/${Character_Callback_character_id}
        method: post
        tests:
        -   name: status_code_is_201
            assert: ${{response.status_code == 201}}
        body:
            callbackUrl: ${Character_Callback_callbackUrl}
    -   name: Episode_Callback
        path: /api/callbacks/episodes/${Episode_Callback_episode_id}
        method: post
        tests:
        -   name: status_code_is_201
            assert: ${{response.status_code == 201}}
        body:
            callbackUrl: ${Episode_Callback_callbackUrl}
    -   name: Season_Callback
        path: /api/callbacks/seasons/${Season_Callback_season_id}
        method: post
        tests:
        -   name: status_code_is_201
            assert: ${{response.status_code == 201}}
        body:
            callbackUrl: ${Season_Callback_callbackUrl}
    -   name: Random_Character
        path: /api/random/character
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
    -   name: Random_Episode
        path: /api/random/episode
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
    -   name: Random_Season
        path: /api/random/season
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
    -   name: Character
        path: /api/characters/${Character_character_id}
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
    -   name: Characters
        path: /api/characters
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
    -   name: Create_Secret_Message
        path: /api/crypto/secret_message
        method: post
        tests:
        -   name: status_code_is_201
            assert: ${{response.status_code == 201}}
        body:
            text: ${Create_Secret_Message_text}
    -   name: Get_Secret_Message
        path: /api/crypto/secret_message/${Get_Secret_Message_url}
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
    -   name: Episode
        path: /api/episodes/${Episode_episode_id}
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
    -   name: Episodes
        path: /api/episodes
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
    -   name: Character_Sse
        path: /api/notifications/sse/characters/${Character_Sse_character_id}
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
    -   name: Season
        path: /api/seasons/${Season_season_id}
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
    -   name: Seasons
        path: /api/seasons
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
    -   name: Get_User_Auth_Token
        path: /api/tokens/users/auth
        method: post
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
        body:
            username: ${Get_User_Auth_Token_username}
            password: ${Get_User_Auth_Token_password}
    -   name: Get_Refreshed_User_Auth_Token
        path: /api/tokens/users/refresh
        method: post
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
        body:
            refresh_token: ${Get_Refreshed_User_Auth_Token_refresh_token}
    -   name: Create_User
        path: /api/users
        method: post
        tests:
        -   name: status_code_is_201
            assert: ${{response.status_code == 201}}
        body:
            name: ${Create_User_name}
            surname: ${Create_User_surname}
            email: ${Create_User_email}
            username: ${Create_User_username}
            password: ${Create_User_password}
    -   name: List_Users
        path: /api/users
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
    -   name: Update_User
        path: /api/users
        method: put
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
        headers:
            Authorization: Bearer ${bearer_token}
    -   name: User_Me
        path: /api/users/me
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
        headers:
            Authorization: Bearer ${bearer_token}
    -   name: Resend_User_Confirmation
        path: /api/users/confirmations/resend
        method: post
        tests:
        -   name: status_code_is_202
            assert: ${{response.status_code == 202}}
        headers:
            Authorization: Bearer ${bearer_token}
    -   name: Request_Change_User_Password
        path: /api/users/passwords/request-change
        method: post
        tests:
        -   name: status_code_is_202
            assert: ${{response.status_code == 202}}
        body:
            email: ${Request_Change_User_Password_email}
    -   name: Create_Link
        path: /api/links
        method: post
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
        body:
            url: ${Create_Link_url}
        headers:
            Authorization: Bearer ${bearer_token}
    -   name: List_Links
        path: /api/links
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
        headers:
            Authorization: Bearer ${bearer_token}
    -   name: Get_Link
        path: /api/links/${Get_Link_link_id}
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
        headers:
            Authorization: Bearer ${bearer_token}
    -   name: Create_Favorite_Character
        path: /api/favorites/characters/${Create_Favorite_Character_character_id}
        method: post
        tests:
        -   name: status_code_is_204
            assert: ${{response.status_code == 204}}
        headers:
            Authorization: Bearer ${bearer_token}
    -   name: Delete_Favorite_Character
        path: /api/favorites/characters/${Delete_Favorite_Character_character_id}
        method: delete
        tests:
        -   name: status_code_is_204
            assert: ${{response.status_code == 204}}
        headers:
            Authorization: Bearer ${bearer_token}
    -   name: List_Favorite_Characters
        path: /api/favorites/characters
        method: get
        tests:
        -   name: status_code_is_200
            assert: ${{response.status_code == 200}}
        headers:
            Authorization: Bearer ${bearer_token}

Points of interest:

  1. Endpoints that require authentication have a headers entry with Authorization: Bearer ${bearer_token}. If we defined the bearer_token anywhere on the file, we have working authentication for these endpoints.
  2. Endpoints with path parameters (such as /api/characters/${Character_character_id}) have the path parameter as a variable. This means we can quickly implement this variable in another endpoint.
  3. Endpoints with a body have that body pre filled with variables.

What type of change is this?

Implementation of a new feature.

Checklist

  • A changelog entry was added, or this PR does not require one. Instructions
  • Unit tests were added or updated as needed, or not required for this change. Instructions
  • All unit tests pass locally. Instructions
  • Docstrings or comments were added or updated as needed, or no documentation changes were required. Instructions
  • This PR does not significantly reduce code or docstring coverage.
  • Code follows the project’s style guidelines.
  • ScanAPI was run locally and the changes were manually verified, or this was not necessary. Instructions

Issue

Closes #<issue_number>

@guites guites changed the title Generate ScanAPI spec from OpenAPI feat: generate ScanAPI spec from OpenAPI Feb 1, 2026
dinalivia and others added 4 commits February 1, 2026 18:14
- Remove black, flake8, isort from dev dependencies
- Add ruff as single linter/formatter
- Update Makefile: 'make lint' and 'make format' use ruff
- Configure ruff in pyproject.toml (matching existing flake8 ignores)
- No linting rule changes - same behavior as before
@guites guites force-pushed the scanapi_spec_from_openapi_v2 branch from 2b51579 to d1c84df Compare February 1, 2026 21:15
@guites guites force-pushed the scanapi_spec_from_openapi_v2 branch from d1c84df to 8078b62 Compare February 1, 2026 21:18
@camilamaia
Copy link
Member

It’s really great to see this finally coming to life. Thanks a lot @guites for pushing this forward and taking it on 💜

As you suggested, I focused on reviewing the solution and UX, not the source code itself. At a first glance, the conversion looks solid! No changes suggested there.

I do have a small suggestion around the CLI design, though. What do you think about structuring the command like this?

scanapi convert <source> <target> <input>

Example:

scanapi convert openapi scanapi openapi.yaml

In which:

<source> source format

  • Defines the format of the input file, not just its extension
  • Scales naturally as new formats are added, without changing the CLI shape

Possible future uses:

scanapi convert postman scanapi collection.json
scanapi convert openapi har api.yaml

<target> target format

  • Makes the output format explicit
  • Allows the same source format to support multiple targets without ambiguity

<input> input file

  • Path to the file being converted
  • Interpreted according to <source>, not inferred from the file extension

I like this structure because it’s explicit, predictable, and scales well as new formats are added.

For the output, we could reuse the --output-path flag from reports. Making it more generic would allow us to reuse it here and keep the CLI consistent.


Just brainstorming future ideas. It could be interesting to automatically generate tests to validate response bodies when they are described in the openapi spec.

Happy to hear your thoughts!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants