Skip to content

Reference

override_validation_error

override_validation_error(
    app,
    status_code=400,
    handle_exceptions=True,
    merge_target_response=True,
    error_builder=None,
    media_type="application/json",
)

Override FastAPI's default 422 validation error response with a custom status code, at runtime and in the OpenAPI schema.

The schema is patched on top of whatever the current app.openapi returns, so a custom OpenAPI function assigned before this call is preserved, and its caching with it.

The first call configures the app. Subsequent calls on the same instance are ignored, even with different arguments. Passing status_code=422 without error_builder and with the default media_type is a no-op too: the app is left unpatched, as in FastAPI.

Raises TypeError if status_code is not an int (a bool is rejected too), or if error_builder is async or its return annotation is not a Pydantic model. Raises ValueError if status_code is outside 100-599 or can't carry a body, if media_type is not a JSON content type, or if error_builder is passed with handle_exceptions=False. Nothing is registered on the app when it raises.

PARAMETER DESCRIPTION
app

The application to patch, in place.

TYPE: FastAPI

status_code

The HTTP status code to use instead of 422. It must be a valid code (100-599) that carries a response body, so 1xx, 204 and 304 are rejected. An int subclass such as http.HTTPStatus is used as its plain value.

TYPE: int DEFAULT: 400

handle_exceptions

If True, registers an exception handler that returns status_code at runtime. If False, only the OpenAPI schema is patched, and you handle the exception yourself.

TYPE: bool DEFAULT: True

merge_target_response

If True, a response you already declared at status_code is merged with the validation error schema using anyOf. If False, it is left untouched.

TYPE: bool DEFAULT: True

error_builder

A regular (not async) function that turns the RequestValidationError into the response body, with a Pydantic model as its return annotation. The handler returns what it builds, and the schema documents that model at status_code instead of HTTPValidationError, on the operations that validate input. FastAPI's HTTPValidationError/ValidationError components are then removed, unless something in the schema still references them. If None, the body is {"detail": [...]}, as in FastAPI.

TYPE: Callable[[RequestValidationError], Any] | None DEFAULT: None

media_type

The content type of the validation error, e.g. "application/problem+json". It must be application/json or end in +json, since the body is JSON. The handler sends it, and the schema documents the error under it. A response already declared at status_code under another content type is kept alongside, not merged with anyOf. With handle_exceptions=False, your own handler has to send it.

TYPE: str DEFAULT: 'application/json'

Source code in fastapi_validation_override/overrider.py
def override_validation_error(
    app: Annotated[FastAPI, Doc("The application to patch, in place.")],
    status_code: Annotated[
        int,
        Doc(
            "The HTTP status code to use instead of 422. It must be a valid code (100-599) that "
            "carries a response body, so 1xx, 204 and 304 are rejected. An `int` subclass such as "
            "`http.HTTPStatus` is used as its plain value."
        ),
    ] = 400,
    handle_exceptions: Annotated[
        bool,
        Doc(
            "If `True`, registers an exception handler that returns `status_code` at runtime. If "
            "`False`, only the OpenAPI schema is patched, and you handle the exception yourself."
        ),
    ] = True,
    merge_target_response: Annotated[
        bool,
        Doc(
            "If `True`, a response you already declared at `status_code` is merged with the "
            "validation error schema using `anyOf`. If `False`, it is left untouched."
        ),
    ] = True,
    error_builder: Annotated[
        Callable[[RequestValidationError], Any] | None,
        Doc(
            "A regular (not async) function that turns the `RequestValidationError` into the "
            "response body, with a Pydantic model as its return annotation. The handler returns what "
            "it builds, and the schema documents that model at `status_code` instead of "
            "`HTTPValidationError`, on the operations that validate input. FastAPI's "
            "`HTTPValidationError`/`ValidationError` components are then removed, unless something "
            'in the schema still references them. If `None`, the body is `{"detail": [...]}`, as '
            "in FastAPI."
        ),
    ] = None,
    media_type: Annotated[
        str,
        Doc(
            'The content type of the validation error, e.g. `"application/problem+json"`. It must '
            "be `application/json` or end in `+json`, since the body is JSON. The handler sends it, "
            "and the schema documents the error under it. A response already declared at "
            "`status_code` under another content type is kept alongside, not merged with `anyOf`. "
            "With `handle_exceptions=False`, your own handler has to send it."
        ),
    ] = "application/json",
) -> None:
    """
    Override FastAPI's default 422 validation error response with a custom status code, at
    runtime and in the OpenAPI schema.

    The schema is patched on top of whatever the current `app.openapi` returns, so a custom
    OpenAPI function assigned before this call is preserved, and its caching with it.

    The first call configures the app. Subsequent calls on the same instance are ignored, even
    with different arguments. Passing `status_code=422` without `error_builder` and with the
    default `media_type` is a no-op too: the app is left unpatched, as in FastAPI.

    Raises `TypeError` if `status_code` is not an `int` (a `bool` is rejected too), or if
    `error_builder` is async or its return annotation is not a Pydantic model. Raises
    `ValueError` if `status_code` is outside 100-599 or can't carry a body, if `media_type` is
    not a JSON content type, or if `error_builder` is passed with `handle_exceptions=False`.
    Nothing is registered on the app when it raises.
    """
    if isinstance(status_code, bool) or not isinstance(status_code, int):
        raise TypeError(f"status_code must be an int, got {type(status_code).__name__}: {status_code!r}")
    status_code = int(status_code)
    if not (100 <= status_code <= 599):
        raise ValueError(f"status_code must be a valid HTTP status code (100-599), got {status_code}")
    if status_code < 200 or status_code in (204, 304):
        raise ValueError(
            f"status_code {status_code} does not support a response body, but this library always sends one"
        )
    if not (media_type == "application/json" or media_type.endswith("+json")):
        raise ValueError(f"media_type must be a JSON content type, since the body is JSON, got {media_type!r}")

    error_model: Any = None
    if error_builder is not None:
        if not handle_exceptions:
            raise ValueError("error_builder requires handle_exceptions=True: it builds the body that handler returns")
        if inspect.iscoroutinefunction(error_builder):
            raise TypeError("error_builder must be a regular function, not async")
        error_model = get_type_hints(error_builder).get("return")
        if not (isinstance(error_model, type) and hasattr(error_model, "model_json_schema")):
            raise TypeError(f"error_builder must declare a Pydantic model as its return type, got {error_model!r}")

    if status_code == 422 and error_builder is None and media_type == "application/json":
        return

    if getattr(app.state, "_validation_overridden", False):
        return

    if handle_exceptions:

        @app.exception_handler(RequestValidationError)
        async def custom_validation_exception_handler(_request: Request, exc: RequestValidationError) -> JSONResponse:
            body = error_builder(exc) if error_builder is not None else {"detail": exc.errors()}
            return JSONResponse(status_code=status_code, content=jsonable_encoder(body), media_type=media_type)

    target_code = str(status_code)
    original_openapi = app.openapi
    resolve_component = (
        _resolve_validation_ref if error_model is None else partial(_resolve_model_ref, model=error_model)
    )

    def custom_openapi() -> dict[str, Any]:
        schema = original_openapi()
        operations = _remove_fastapi_422_responses(schema)
        if error_model is not None:
            # Before the model is registered, so its names don't collide with components about to go
            _drop_unreferenced_validation_components(schema)
        _document_validation_responses(
            schema, operations, target_code, merge_target_response, resolve_component, media_type
        )
        app.openapi_schema = schema
        return app.openapi_schema

    app.openapi = custom_openapi  # type: ignore[method-assign]  # ty: ignore[invalid-assignment]
    app.state._validation_overridden = True

patch_422_responses

patch_422_responses(
    schema, target_code, *, merge_target_response=True
)

Move FastAPI's 422 validation error response to target_code, across paths, webhooks, and callbacks (recursively, for callbacks declared on any operation). Returns the same schema, mutated in place.

Designed for schemas produced by fastapi.openapi.utils.get_openapi() — the same generator app.openapi() uses internally. It's exposed publicly so it can be patched outside override_validation_error, e.g. from a custom app.openapi function or a tool that builds its own per-version schema via get_openapi(). It is not meant as a general-purpose processor for arbitrary or hand-authored OpenAPI documents: correctness on documents built by hand or by other tools is best-effort, not a guarantee.

A parameter declared with include_in_schema=False still triggers validation at runtime, but is invisible in the generated schema. If such a route also already occupies 422, 4XX, or default with something else, FastAPI never emits its own 422 response for that operation — the only signal this function relies on to detect the need for validation on such a parameter. In that specific combination, target_code won't be documented.

PARAMETER DESCRIPTION
schema

An OpenAPI schema produced by fastapi.openapi.utils.get_openapi(), such as the one returned by app.openapi().

TYPE: dict[str, Any]

target_code

The status code to document the validation error at, e.g. "400".

TYPE: str

merge_target_response

If True, a response already declared at target_code is merged with the validation error schema using anyOf. If False, it is left untouched. A response declared as a $ref is always left untouched: get_openapi() never produces one at a route's own declared status code, and this module doesn't resolve $refs.

TYPE: bool DEFAULT: True

Source code in fastapi_validation_override/overrider.py
def patch_422_responses(
    schema: Annotated[
        dict[str, Any],
        Doc(
            "An OpenAPI schema produced by `fastapi.openapi.utils.get_openapi()`, such as the one "
            "returned by `app.openapi()`."
        ),
    ],
    target_code: Annotated[str, Doc('The status code to document the validation error at, e.g. `"400"`.')],
    *,
    merge_target_response: Annotated[
        bool,
        Doc(
            "If `True`, a response already declared at `target_code` is merged with the validation "
            "error schema using `anyOf`. If `False`, it is left untouched. A response declared as a "
            "`$ref` is always left untouched: `get_openapi()` never produces one at a route's own "
            "declared status code, and this module doesn't resolve `$ref`s."
        ),
    ] = True,
) -> dict[str, Any]:
    """
    Move FastAPI's 422 validation error response to `target_code`, across paths, webhooks, and
    callbacks (recursively, for callbacks declared on any operation). Returns the same `schema`,
    mutated in place.

    Designed for schemas produced by `fastapi.openapi.utils.get_openapi()` — the same generator
    `app.openapi()` uses internally. It's exposed publicly so it can be patched outside
    `override_validation_error`, e.g. from a custom `app.openapi` function or a tool that builds its
    own per-version schema via `get_openapi()`. It is not meant as a general-purpose processor for
    arbitrary or hand-authored OpenAPI documents: correctness on documents built by hand or by other
    tools is best-effort, not a guarantee.

    A parameter declared with `include_in_schema=False` still triggers validation at runtime, but
    is invisible in the generated schema. If such a route also already occupies 422, `4XX`, or
    `default` with something else, FastAPI never emits its own 422 response for that operation —
    the only signal this function relies on to detect the need for validation on such a parameter.
    In that specific combination, `target_code` won't be documented.
    """
    operations = _remove_fastapi_422_responses(schema)
    _document_validation_responses(schema, operations, target_code, merge_target_response, _resolve_validation_ref)
    return schema