3

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?

Chris
  • 18,724
  • 6
  • 46
  • 80
Tom McLean
  • 5,583
  • 1
  • 11
  • 36
  • Try creating Response object for the endpoint that returns RFCProblemJSOn and specify the content and media_type, instead of directly providing to fastapi. – Prudhviraj Mar 13 '23 at 15:53
  • @Prudhviraj by default, the endpoints dont return a RFC Problem JSON, only when an error happens it will return RFC Problem JSON. My code has an exception handler that catches errors and converts them into RFC Problem JSON. – Tom McLean Mar 13 '23 at 15:55
  • If you already have an exception handler, similar to [this](https://stackoverflow.com/a/70954531/17865804) or [this](https://stackoverflow.com/a/71682274/17865804), you could then return a custom `Response` (as shown in Option 2 of [this answer](https://stackoverflow.com/a/71883126/17865804), for instance) from the exception handler, specifying the desired `media_type`. – Chris Mar 13 '23 at 16:46

1 Answers1

3

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

Chris
  • 18,724
  • 6
  • 46
  • 80
  • Thanks for the answer. I am having a bug with this where if I put a basemodel field inside Message, then swagger ui returns an error (i.e. `Could not resolve reference: Could not resolve pointer: /definitions/SubMessage does not exist in document`) – Tom McLean Mar 14 '23 at 14:46
  • 1
    Thanks for letting me know. Please have a look at the updated example above. – Chris Mar 14 '23 at 19:21