Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Openapi 3.x response doesn't place schema in content: application/json block #258

Open
caarmen opened this issue Nov 11, 2024 · 0 comments

Comments

@caarmen
Copy link

caarmen commented Nov 11, 2024

Summary

When using openapi 3, the schema inside the responses should be inside a content, then application/json block (or other media type if not application/json).

Openapi response object documentation is here.
☝🏻 If I've missed some configuration that allows to do this, my apologies!

Code to reproduce the issue

We define a route to list pets.
We configure the app to use openapi 3.

petsbp.py:

from flask import Blueprint, jsonify
from flask_apispec import doc, marshal_with
from marshmallow import Schema, fields


bp = Blueprint(
    "pets",
    __name__,
    url_prefix="/pets",
)

class PetSchema(Schema):
    name = fields.Str(required=True)


@bp.route("/", methods=("GET",))
@doc(description="list pets")
@marshal_with(schema=PetSchema(many=True), code=200)
def list_pets():
    return [{"name": "Fido"}]

server.py:

def create_app():
    app = Flask(__name__)
    app.register_blueprint(petsbp.bp)
    app.config.update(
        {
            "APISPEC_SPEC": APISpec(
                title="PetsAPI",
                version="v1",
                openapi_version="3.0.0",
                plugins=[MarshmallowPlugin()],
            ),
        }
    )
    docs = FlaskApiSpec(
        app,
        document_options=False,
    )
    with app.app_context():
        docs.register_existing_resources()

    return app

Results

Expected behavior

Generated schema:

{
    "components": {
        "schemas": {
            "Pet": {
                "properties": {
                    "name": {
                        "type": "string"
                    }
                },
                "required": [
                    "name"
                ],
                "type": "object"
            }
        }
    },
    "info": {
        "title": "PetsAPI",
        "version": "v1"
    },
    "openapi": "3.0.0",
    "paths": {
        "/pets/": {
            "get": {
                "description": "list pets",
                "parameters": [],
                "responses": {
                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "items": {
                                        "$ref": "#/components/schemas/Pet"
                                    },
                                    "type": "array"
                                }
                            }
                        },
                        "description": "list pets"
                    }
                }
            }
        }
    }
}

Zoom in on the 200 response only:

                    "200": {
                        "content": {
                            "application/json": {
                                "schema": {
                                    "items": {
                                        "$ref": "#/components/schemas/Pet"
                                    },
                                    "type": "array"
                                }
                            }
                        },
                        "description": "list pets"
                    }

Note that the schema appears inside a block application/json, which is inside content, which is inside 200.

Swagger ui:

image
image

Note that we have an example value and schema.

Actual behavior:

Generated schema:

{
    "components": {
        "schemas": {
            "Pet": {
                "properties": {
                    "name": {
                        "type": "string"
                    }
                },
                "required": [
                    "name"
                ],
                "type": "object"
            }
        }
    },
    "info": {
        "title": "PetsAPI",
        "version": "v1"
    },
    "openapi": "3.0.0",
    "paths": {
        "/pets/": {
            "get": {
                "description": "list pets",
                "parameters": [],
                "responses": {
                    "200": {
                        "description": "",
                        "schema": {
                            "items": {
                                "$ref": "#/components/schemas/Pet"
                            },
                            "type": "array"
                        }
                    }
                }
            }
        }
    }
}

Zoom in on the 200 response only:

                    "200": {
                        "description": "",
                        "schema": {
                            "items": {
                                "$ref": "#/components/schemas/Pet"
                            },
                            "type": "array"
                        }
                    }

Note that the schema is a direct child of 200.

Swagger UI:

image

Note there's no example value or schema.

Possible workarounds

Workaround 1 - specify the schema in @doc, don't use @use_marshal.

Use @doc to specify the response. Don't use @use_marshal:

@bp.route("/", methods=("GET",))
@doc(
    description="list pets",
    responses={
        200: {
            "content": {"application/json": {"schema": PetSchema(many=True)}},
            "description": "list pets",
        }
    },
)
def list_pets():
    return jsonify(PetSchema(many=True).dump([{"name": "Fido"}]))

Workaround 2 - use a custom converter

server.py:

class MyViewConverter(ViewConverter):
    def get_path(self, rule, target, **kwargs):
        """
        In all responses, move the `schema` inside a `content`, `application/json` block.
        TODO handle other mime types
        """
        result = super().get_path(rule, target, **kwargs)
        for operation in result["operations"].values():
            for response in operation["responses"].values():
                schema = response.pop("schema")
                content = response.setdefault("content", {})
                mimetype = content.setdefault("application/json", {})
                mimetype["schema"] = schema
        return result


def create_app():
    app = Flask(__name__)
    app.register_blueprint(petsbp.bp)
    app.config.update(
        {
            "APISPEC_SPEC": APISpec(
                title="PetsAPI",
                version="v1",
                openapi_version="3.0.0",
                plugins=[MarshmallowPlugin()],
            ),
        }
    )
    docs = FlaskApiSpec(
        app,
        document_options=False,
    )
    # NEW: use our custom view converter
    docs.view_converter = MyViewConverter(docs.app, docs.spec, docs.document_options)
    with app.app_context():
        docs.register_existing_resources()

    return app
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

No branches or pull requests

1 participant