Skip to content

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

Open
@caarmen

Description

@caarmen

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions