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?