Set the media type of a custom Error Response via a pydantic model in FastAPI

Question:

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::
Incorrect media type

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?

Asked By: Tom McLean

||

Answers:

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,
like 200, and the values are other dicts with the information for
each of them.

Each of those response dicts can have a key model, containing a
Pydantic model, just like response_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.

Working Example

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

Answered By: Chris