In my FastAPI application I want to return my errors as RFC Problem JSON:
from pydantic import BaseModel
class RFCProblemJSON(BaseModel):
type: str
title: str
detail: str | None
status: int | None
I can set the response model in the OpenAPI docs with the responses
argument of the FastAPI class:
from fastapi import FastAPI, status
api = FastAPI(
responses={
status.HTTP_401_UNAUTHORIZED: {'model': RFCProblemJSON},
status.HTTP_422_UNPROCESSABLE_ENTITY: {'model': RFCProblemJSON},
status.HTTP_500_INTERNAL_SERVER_ERROR: {'model': RFCProblemJSON}
}
)
However, I want to set the media type as 'application/problem+json'. I tried two methods, first just adding a 'media type' field on to the basemodel:
class RFCProblemJSON(BaseModel):
media_type = "application/problem+json"
type: str
title: str
detail: str | None
status: int | None
and also, inheriting from fastapi.responses.Response
:
class RFCProblemJSON(Response):
media_type = "application/problem+json"
type: str
title: str
detail: str | None
status: int | None
However neither of these modify the media_type in the openapi.json file/the swagger UI.
When you add the media_type field to the basemodel, the media type in the SwaggerUI is not modified::
And when you make the model inherit from Response, you just get an error (this was a long shot from working but tried it anyway).
raise fastapi.exceptions.FastAPIError(
fastapi.exceptions.FastAPIError: Invalid args for response field! Hint: check that <class 'RoutingServer.RestAPI.schema.errors.RFCProblemJSON'> is a valid Pydantic field type. If you are using a return type annotation that is not a valid Pydantic field (e.g. Union[Response, dict, None]) you can disable generating the response model from the type annotation with the path operation decorator parameter response_model=None. Read more: https://fastapi.tiangolo.com/tutorial/response-model/
It is possible to get the swagger UI to show the correct media type if you manually fill out the OpenAPI definition:
api = FastAPI(
debug=debug,
version=API_VERSION,
title="RoutingServer API",
openapi_tags=tags_metadata,
swagger_ui_init_oauth={"clientID": oauth2_scheme.client_id},
responses={
status.HTTP_401_UNAUTHORIZED: {
"content": {"application/problem+json": {
"example": {
"type": "string",
"title": "string",
"detail": "string"
}}},
"description": "Return the JSON item or an image.",
},
}
)
However, I want to try and implement this with a BaseModel so that I can inherit from RFCProblemJSON and provide some optional extras for some specific errors.
The minimal example to reproduce my problem is:
from pydantic import BaseModel
from fastapi import FastAPI, status, Response, Request
from fastapi.exceptions import RequestValidationError
from pydantic import error_wrappers
import json
import uvicorn
from typing import List, Tuple, Union, Dict, Any
from typing_extensions import TypedDict
Loc = Tuple[Union[int, str], ...]
class _ErrorDictRequired(TypedDict):
loc: Loc
msg: str
type: str
class ErrorDict(_ErrorDictRequired, total=False):
ctx: Dict[str, Any]
class RFCProblemJSON(BaseModel):
type: str
title: str
detail: str | None
status: int | None
class RFCUnprocessableEntity(RFCProblemJSON):
instance: str
issues: List[ErrorDict]
class RFCProblemResponse(Response):
media_type = "application/problem+json"
def render(self, content: RFCProblemJSON) -> bytes:
return json.dumps(
content.dict(),
ensure_ascii=False,
allow_nan=False,
indent=4,
separators=(", ", ": "),
).encode("utf-8")
api = FastAPI(
responses={
status.HTTP_422_UNPROCESSABLE_ENTITY: {'model': RFCUnprocessableEntity},
}
)
@api.get("/{x}")
def hello(x: int) -> int:
return x
@api.exception_handler(RequestValidationError)
def format_validation_error_as_problem_json(request: Request, exc: error_wrappers.ValidationError):
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
content = RFCUnprocessableEntity(
type="/errors/unprocessable_entity",
title="Unprocessable Entity",
status=status_code,
detail="The request has validation errors.",
instance=request.url.path,
issues=exc.errors()
)
return RFCProblemResponse(content, status_code=status_code)
uvicorn.run(api)
When you go to http://localhost:8000/hello
, it will return as application/problem+json
in the headers, however if you go to the swagger ui docs the ui shows the response will be application/json
. I dont know how to keep the style of my code, but update the openapi definition to show that it will return as 'application/problem+json` in a nice way.
Is this possible to do?
As described in FastAPI's documentation about Additional Responses in OpenAPI:
You can pass to your path operation decorators a parameter
responses
.It receives a
dict
, the keys are status codes for each response, like200
, and the values are otherdict
s with the information for each of them.Each of those response
dict
s can have a keymodel
, containing a Pydantic model, just likeresponse_model
.FastAPI will take that model, generate its JSON Schema and include it in the correct place in OpenAPI.
Also, as described in Additional Response with model (see under Info):
The
model
key is not part of OpenAPI.FastAPI will take the Pydantic model from there, generate the
JSON Schema
, and put it in the correct place.The correct place is:
In the key content, that has as value another JSON object (
dict
) that contains:
A key with the media type, e.g.
application/json
, that contains as value another JSON object, that contains:
A key
schema
, that has as the value the JSON Schema from the model, here's the correct place.
- FastAPI adds a reference here to the global JSON Schemas in another place in your OpenAPI instead of including it directly. This way, other applications and clients can use those JSON Schemas directly, provide better code generation tools, etc.
Hence, there doesn't currently seem to be a way to achieve what you are asking— i.e., adding a media_type
field to the BaseModel
, in order to set the media type of an error response (e.g., 422 UNPROCESSABLE ENTITY
) to application/problem+json
—since the model
key is only used to generate the schema
. There has been an extensive discussion on github on a similar issue, where people provide a few solutions, which mainly focus on changing the 422
error response schema, similar to the one described in your question, but in a more elegant way (see this comment, for instance). The example below demonstrates a similar approach that can be easily adapted to your needs.
from fastapi import FastAPI, Response, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.openapi.constants import REF_PREFIX
from fastapi.responses import JSONResponse
from pydantic import BaseModel
import json
class Item(BaseModel):
id: str
value: str
class SubMessage(BaseModel):
msg: str
class Message(BaseModel):
msg: str
sub: SubMessage
class CustomResponse(Response):
media_type = 'application/problem+json'
def render(self, content: Message) -> bytes:
return json.dumps(
content.dict(),
ensure_ascii=False,
allow_nan=False,
indent=4,
separators=(', ', ': '),
).encode('utf-8')
def get_422_schema():
return {
'model': Message,
'content': {
'application/problem+json': {
'schema': {'$ref': REF_PREFIX + Message.__name__}
}
},
}
app = FastAPI(responses={status.HTTP_422_UNPROCESSABLE_ENTITY: get_422_schema()})
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
msg = Message(msg='main message', sub=SubMessage(msg='sub message'))
return CustomResponse(content=msg, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
@app.post('/items')
async def submit(item: Item):
return item