Openapi 3.x response doesn't place schema in content: application/json block
- Dominant language
- Python
- Stars
- 652
- Forks
- 151
- PR merge metrics
- No merged PRs in 30d
Description
# 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](https://swagger.io/specification/#response-object).
☝🏻 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`:
```python
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`:
```python
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:
```json
{
"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:
```json
"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:


Note that we have an example value and schema.
## Actual behavior:
### Generated schema:
```json
{
"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:
```json
"200": {
"description": "",
"schema": {
"items": {
"$ref": "#/components/schemas/Pet"
},
"type": "array"
}
}
```
Note that the `schema` is a direct child of `200`.
### Swagger UI:
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`:
```python
@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`:
```python
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
```
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the reproduction in petsbp.py and server.py, especially marshal_with, APISpec, and docs.register_existing_resources. Compare the generated response with the OpenAPI 3 response-object documentation and the expected JSON; done means the schema is nested under content and application/json, with the response description and Swagger UI schema preserved.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- openapi, python
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100