Skip to content

feat: implements openapi to scanapi.yaml convertion#866

Merged
camilamaia merged 46 commits into
scanapi:mainfrom
guites:feat/convert-openapi-spec
May 8, 2026
Merged

feat: implements openapi to scanapi.yaml convertion#866
camilamaia merged 46 commits into
scanapi:mainfrom
guites:feat/convert-openapi-spec

Conversation

@guites

@guites guites commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Description

Implements a OpenAPI specification to ScanAPI.yaml convertion.

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.json -o scanapi-futurama.yaml -b https://futuramaapi.com
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:  https://futuramaapi.com
    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 #866

@guites guites marked this pull request as ready for review March 4, 2026 21:28
@guites guites requested review from a team as code owners March 4, 2026 21:28
@guites

guites commented Mar 4, 2026

Copy link
Copy Markdown
Contributor Author

Hey :) I think the code reached its final form. I'm marking it as ready for commit, but I'll still add tests for the convert method, which should bump coverage back to 98%!

@guites

guites commented Apr 12, 2026

Copy link
Copy Markdown
Contributor Author

I've fixed most of deepsource problems and ignored all documentation/formatting related issues (see https://app.deepsource.com/gh/scanapi/scanapi/run/0ae4f501-371d-478b-b552-235b85f2473f/python/). I think we can go ahead and skip deepsource output altogether for this PR.

@guites

guites commented Apr 12, 2026

Copy link
Copy Markdown
Contributor Author

Our convertion pipeline is based on prance (https://github.com/RonnyPfannschmidt/prance) using the openapi-spec-validator backend (https://github.com/p1c2u/openapi-spec-validator).

The default behavior is to block parsing of schemas with non-compliant fields.

For example the following schema (the "example" field is defined incorrectly under a response definition. see https://spec.openapis.org/oas/v3.1.0#response-object) :

openapi: 3.0.4
info:
  title: Sample API
  version: 0.1.9

paths:
  /users:
    get:
      summary: Returns a list of users.
      description: Optional extended description in CommonMark or HTML.
      responses:
        "200":
          description: A JSON array of user names
          example: "teste"
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string

Would result in the following error (when running uv run scanapi from example.yaml):

ERROR Couldn't parse OpenAPI schema: ("{'description': 'A JSON array of user names', 'example': 'teste', 'content': {'application/json': {'schema': {'type': 'array', 'items': {'type': 'string'}}}}} is
not valid under any of the given schemas", 'oneOf', deque(['paths', '/users', 'get', 'responses', '200']), None, [<ValidationError: "'example' does not match any of the regexes: '^x-'">,
<ValidationError: "'$ref' is a required property">], [{'$ref': '#/definitions/Response'}, {'$ref': '#/definitions/Reference'}], {'description': 'A JSON array of user names', 'example': 'teste',
'content': {'application/json': {'schema': {'type': 'array', 'items': {'type': 'string'}}}}}, {'oneOf': [{'$ref': '#/definitions/Response'}, {'$ref': '#/definitions/Reference'}]},
deque(['properties', 'paths', 'patternProperties', '^\/', 'patternProperties', '^(get|put|post|delete|options|head|patch|trace)$', 'properties', 'responses', 'patternProperties',
'^1-5$', 'oneOf']), None)

The specific error (in this case <ValidationError: "'example' does not match any of the regexes: '^x-'">, <ValidationError: "'$ref' is a required property">) depends on the OpenAPI version.

If we change version 3.0.4 to 3.1.0, keeping the same schema, we get the following error:

ERROR Couldn't parse OpenAPI schema: ("Unevaluated properties are not allowed ('example' was unexpected)", 'unevaluatedProperties', deque(['paths', '/users', 'get', 'responses', '200']), None, [],
False, {'description': 'A JSON array of user names', 'example': 'teste', 'content': {'application/json': {'schema': {'type': 'array', 'items': {'type': 'string'}}}}}, {'$comment':
'https://spec.openapis.org/oas/v3.1.0#response-object', 'type': 'object', 'properties': {'description': {'type': 'string'}, 'headers': {'type': 'object', 'additionalProperties': {'$ref':
'#/$defs/header-or-reference'}}, 'content': {'$ref': '#/$defs/content'}, 'links': {'type': 'object', 'additionalProperties': {'$ref': '#/$defs/link-or-reference'}}}, 'required': ['description'],
'$ref': '#/$defs/specification-extensions', 'unevaluatedProperties': False}, deque(['properties', 'paths', 'patternProperties', '^/', 'else', 'properties', 'get', 'properties', 'responses',
'patternProperties', '^1-5$', 'else', 'unevaluatedProperties']), None)

The error changed to "Unevaluated properties are not allowed ('example' was unexpected)".

I think it's pretty common for OpenAPI specs to have incorrect/incompatible fields, since there are so many spec generation tools out there.

cc @camilamaia wdyt? Should we move forward with a "strict" implementation for now and study having a --lax option later on, or do you think this is a blocker?

prance seems to accept a Strict flag (see https://github.com/RonnyPfannschmidt/prance/blob/main/prance/__init__.py#L128) but it still isn't clear how it works or how we could send this flag over.

edit: the strict flag on prance works by stringifying integer keys, so it's unrelated to our non-default keys problem

Comment thread tests/data/convert/invalid.json
Comment thread scanapi/convert.py
@camilamaia

Copy link
Copy Markdown
Member

Great point, @guites. Some thoughts that might help move it forward:

  1. Would it make sense to replace the inline TODOs with GitHub issues?
    That keeps the codebase cleaner, avoids static analysis complaints (e.g. DeepSource), and also creates a clearer backlog of follow-up improvements.

  2. I think keeping validation strict for this first version is a reasonable tradeoff, since it reduces complexity and keeps behavior predictable.
    That said, it may help to improve the current error message a bit so users understand what is required, instead of only surfacing the upstream parser/validator exception.

For example, something like:

Invalid OpenAPI schema.
Conversion currently requires a valid OpenAPI 3.x document.
Details: <validator error>

  1. Since you already mentioned the idea of a more flexible validation mode, would it make sense to open a follow-up issue for that?
    I think that would be a good place to discuss options such as lax / best-effort parsing for partially invalid specs that are still usable for skeleton generation, while keeping this PR focused on the strict first version.

Overall, I’d be in favor of keeping the current implementation strict for now and tracking the flexibility improvements separately.

@guites

guites commented Apr 24, 2026

Copy link
Copy Markdown
Contributor Author

@camilamaia I agree with all points :) Will try to do the changes asap.

The error message now reads

ERROR Invalid OpenAPI schema.
Conversion currently requires a valid OpenAPI 3.x document.
Details: ("'' does not match '[^/#?]+$'", 'pattern', deque(['paths', '/api/characters/{}', 'get', 'parameters', 0, 'name']), None, [], '[^/#?]+$', '', {'pattern': '[^/#?]+$'},
deque(['properties', 'paths', 'patternProperties', '^/', 'else', 'properties', 'get', 'properties', 'parameters', 'items', 'else', 'dependentSchemas', 'schema', 'allOf', 1, 'then', 'properties',
'name', 'pattern']), None)

@guites

guites commented Apr 24, 2026

Copy link
Copy Markdown
Contributor Author

cc @camilamaia I think I made all mentioned changes :) lmk if anything else comes up

@guites

guites commented Apr 24, 2026

Copy link
Copy Markdown
Contributor Author

deepsource seems to be pleased

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for supporting ScanAPI, and congratulations on your first contribution! A project committer will shortly review your contribution.

In the mean time, if you haven't had a chance please skim over the First Pull Request Guide which all pull requests must adhere to.

We hope to see you around!

@github-actions github-actions Bot added the First Contribution First contribution to the project. label Apr 27, 2026
@camilamaia camilamaia self-requested a review May 8, 2026 12:47

@camilamaia camilamaia left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

YAY!!!!!!

@camilamaia camilamaia merged commit 218b232 into scanapi:main May 8, 2026
9 checks passed
@dinalivia dinalivia mentioned this pull request May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

First Contribution First contribution to the project.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants