fastapi: Allow customization of validation error

Is your feature request related to a problem

I’d like to globally customize validation errors by changing (overriding) the HTTPValidationError/ValidationError and the 422 status code.

The solution you would like

Something great would be adding some parameters to the FastAPI class:

from fastapi import FastAPI

from .models import ValidationError

app = FastAPI(
    validation_error_status_code=422,
    validation_error_model=ValidationError
)

Describe alternatives you’ve considered

What’s currently possible (to my knowledge) is adding an item with status code 422, 4XX or default in the responses dict, but this as to be done manually for every route that will perform validation. This also prevent changing the status code to a specific value (you can either stick with 422, or have something vague like default or 4XX).

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 15
  • Comments: 16

Most upvoted comments

If all you errors follow the same schema, and provided you implemented the custom handler described in https://fastapi.tiangolo.com/tutorial/handling-errors/#override-request-validation-exceptions, you can just override the default schema in openapi/utils.

For example if I take the example server from the docs:

from typing import Optional
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: Optional[str] = None):
    return {"item_id": item_id, "q": q}

it will present the default schema il http://localhost:8000/docs:

image

But if I just overwrite the validation_error_response_definition in the fastapi.openapi.utils module it changes the default schema used by OpenAPI:

from typing import Optional
from fastapi import FastAPI

# import the module we want to modify
import fastapi.openapi.utils as fu

app = FastAPI()

# and override the schema
fu.validation_error_response_definition = {
    "title": "HTTPValidationError",
    "type": "object",
    "properties": {
        "error": {"title": "Message", "type": "string"}, 
    },
}

@app.get("/")
def read_root():
    return {"Hello": "World"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: Optional[str] = None):
    return {"item_id": item_id, "q": q}

gives:

image

As this is still the topic… For all of you, who, like me, just want to change the status code to 400, but keep the default response schema AND still have it in the OpenAPI Docs, without the default 422… 😄 Here it goes…

Firstly, you describe your Pydantic models, like the original (used only for docs):

from pydantic import BaseModel
from typing import Optional, List

class Error(BaseModel):

    loc: Optional[List[str]] = None
    msg: str
    type: str

class ErrorResponse(BaseModel):
    detail: List[Error]

Then you use Pydantic model ErrorResponse in responses when defining your app:

app = FastAPI(title="My API", description="Some description here",
                     responses={status.HTTP_400_BAD_REQUEST: {"model": ErrorResponse}})

Then you override the default exception handler (like in the tutorial):

from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    return JSONResponse(content=jsonable_encoder({"detail": exc.errors()}),
                        status_code=status.HTTP_400_BAD_REQUEST)

With this, you will have 400 normally documented for all your routes.

If you want to remove all 422 codes from the documentation, you need to exchange the default OpenAPI schema, similar to how @tupui wrote above, or according to the tutorial here. Therefore, after defining all your path routes, add the following:

def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
    openapi_schema = get_openapi(
        title=app.title,
        version=app.version,
        routes=app.routes
    )

    http_methods = ["post", "get", "put", "delete"]
    # look for the error 422 and removes it
    for method in openapi_schema["paths"]:
        for m in http_methods:
            try:
                del openapi_schema["paths"][method][m]["responses"]["422"]
            except KeyError:
                pass

    app.openapi_schema = openapi_schema
    return app.openapi_schema


app.openapi = custom_openapi

However, this works for the default API with no additional root paths. In order to make it work for such scenario, or when you want this to work only on the mounted API, you have to add servers to the openapi_schema as well, to have the proper routing in Swagger/Redoc. For example:

def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
    openapi_schema = get_openapi(
        title=app.title,
        version=app.version,
        routes=app.routes,
        servers=[{
            "url": "/subapi"
        }]
    )
    # the rest as above

P.S. If you really wanna be persistent, and remove even the ValidationError and HTTPValidationError from schemas, you can add the following to your custom_openapi() method:

    for schema in list(openapi_schema["components"]["schemas"]):
        if schema == "HTTPValidationError" or schema == "ValidationError":
            del openapi_schema["components"]["schemas"][schema]

Hope this helps! 😄

Thanks again @tiangolo for all the awesome work on the framework! 🚀

The solution that would require less work and have bigger chances on being approved (based only on what I’ve seen here) is creating a flag to not add the 422 response in the swagger documentation.

The custom exception handler already handles the first problem described.

Well it works regarding what’s produced by the API, but it doesn’t regarding documentation.

Following up on @ushu excellent simple solution, I would like to add that you could directly fetch the schema from your error model, ensuring consistent behavior between your response and its documentation.

from pydantic import BaseModel, Field
from fastapi import FastAPI


# define or import your error response model
class ErrorResponse(BaseModel):
    message: str = Field(..., example="message to the developer")
    error: str = Field(..., example="Validation Error")
    detail: dict = Field(..., example=[
        {'loc': 'quantity', 
         'msg': 'value is not a valid integer',
         'type':'type_error.integer'}])

# override fastapi 422 schema
import fastapi.openapi.utils as fu
fu.validation_error_response_definition = ErrorResponse.schema()

app = FastAPI()

Gives:

result

I would like this feature too, because it took more time to make it work than I wanted to

Here is a less hacky way to do it. You can override the 422 response type globally when instantiating a FastAPI application with the responses parameter:

app = FastAPI(
    exception_handlers=(RequestValidationError, validation_exception_handler),
    responses={
        422: {"content": change_422_schema({})},
     },
)

Then, you need to modify the responses dict content, which will make it directly to the OpenAPI schema, by changing the "application/json" / "schema" / "$ref" value to your own Error type name:

from fastapi.openapi.constants import REF_PREFIX

def change_422_schema(content: dict):
    """Change the ValidationError's schema to make it consistent with the rest of our API."""
    new_content = content.copy()
    content_type = new_content.setdefault("application/json", {})
    content_type["schema"] = {"$ref": REF_PREFIX + ErrorResponse.__name__}
    return new_content

So the 422 responses will be in the proper format and the generated OpenAPI schema will show the exact model and there will be no leftover models from FastAPI.

I did similar for a single router but slightly simpler

app = FastAPI(...)
app.include_router(
        api_router,
        prefix=settings.API_V1_STR,
        responses={
            422: {
                "description": "Validation Error",
                "model": ErrorResponse
            }
        }
    )

In my application ovverhide the 422 error for 400, and overhide the responses errors for all api for the example: message: { field_error: message_error}

def get_message(root: str, fields: list, error_message: str, return_message=None):
    return_message = return_message or {}

    if root not in return_message:
        return_message[root] = {}

    for i, field in enumerate(fields):
        if len(fields) == 1:
            return_message[root].update({field: error_message})
        else:
            new_fields = fields.copy()
            del(new_fields[i])
            response = get_message(
                root=field, fields=new_fields,
                error_message=error_message, return_message=return_message[root]
            )
            return_message[root].update(response)
            break

    return return_message

And the method bellow is the rewrite exception_handlers={RequestValidationError: validation_bad_request_exception_handler}

async def validation_bad_request_exception_handler(request, exc):
    message = {}
    for x in exc.errors():
        fields = [entry for entry in x['loc'] if entry != 'body']

        if len(fields) == 0:
            message.update({'error': 'invalid request'})
        elif len(fields) == 1:
            message.update({fields[0]: x['msg']})
        else:
            # for dict / list with errors inside it
            field_root = fields[0]
            del(fields[0])

            message = get_message(
                root=field_root,
                fields=fields,
                error_message=x['msg'],
                return_message=message
            )

    return JSONResponse(
        status_code=400,
        content={
            "message": message,
        },
    )

Its work fine for me =D It is just missing to change the documentation

I am doing something simpler. Just removing any ref to the error 422 in the generated OpenAPI dict:

from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
from fastapi.exceptions import RequestValidationError

app = FastAPI()


@app.get("/items/")
async def read_items():
    return [{"name": "Foo"}]


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    """Your custom validation exception."""
    message = 
    return JSONResponse(
        status_code=400,
        content={"message": f"Validation error: {exc}"}
    )


def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
    openapi_schema = get_openapi()

    # look for the error 422 and removes it
    for method in openapi_schema["paths"]:
        try:
            del openapi_schema["paths"][method]["post"]["responses"]["422"]
        except KeyError:
            pass

    app.openapi_schema = openapi_schema
    return app.openapi_schema


app.openapi = custom_openapi

I have hit a similar issue previously and solved it by modifying the OpenAPI spec ‘manually’ like it’s been suggested in some previous issues.

However, I think it would be useful if you could add ‘additional status codes’ and/or ‘additional responses’ to a APIRouter() instance, which would add them to all of the routes automatically.