Skip to content

API schemas that forbid extra fields

The request and response models of a public API set model_config = ConfigDict(extra="forbid") and live under api/<version>/schemas/requests/ and api/<version>/schemas/responses/.

What to read

  • CreateUserRequest is an inbound model. A payload with a key that the model does not declare fails validation with a 422.
  • UserResponse is an outbound model with the same setting. FastAPI already limits the output to the fields of the response model, so on a response forbid does something else: a handler that returns a dict with a key the model does not declare fails with a 500, and model_validate(row, from_attributes=True) ignores the setting and drops the extra attributes.
  • The router of api/<version>/routers/ imports the schemas and defines none. It builds the response with the constructor, one field at a time.

What to adapt

  • CreateUserRequest and UserResponse are one example. Set model_config on every model of the schema modules of the API.
  • The models of the domain and the models internal to a service do not take the setting.
  • A field typed EmailStr needs the email extra of pydantic (pydantic[email]); the example uses str with a length.

References

code/app/api/v1/schemas/requests/users.py

an inbound model: extra=forbid rejects a key that the model does not declare with a 422

"""The request schemas of the endpoints of users."""

from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Field


class CreateUserRequest(BaseModel):
    """The payload that creates a user. A key that the model does not declare is a 422."""

    model_config = ConfigDict(extra="forbid")

    email: str = Field(min_length=3, max_length=254)
    display_name: str = Field(min_length=1, max_length=80)

code/app/api/v1/schemas/responses/users.py

an outbound model with the same setting

"""The response schemas of the endpoints of users."""

from __future__ import annotations

from pydantic import BaseModel, ConfigDict


class UserResponse(BaseModel):
    """A user as the API returns it."""

    model_config = ConfigDict(extra="forbid")

    id: int
    email: str
    display_name: str

code/app/api/v1/routers/users.py

the router imports the schemas and builds the response field by field

"""The endpoints of users."""

from itertools import count

from fastapi import APIRouter, status

from app.api.v1.schemas.requests.users import CreateUserRequest
from app.api.v1.schemas.responses.users import UserResponse

router = APIRouter(prefix="/users", tags=["users"])

_ids = count(1)


@router.post("", status_code=status.HTTP_201_CREATED)
def create_user(payload: CreateUserRequest) -> UserResponse:
    """Create a user and return it, built field by field from what was stored."""
    user_id = next(_ids)
    return UserResponse(id=user_id, email=payload.email, display_name=payload.display_name)

https://fastapi.tiangolo.com/tutorial/response-model/#return-type-and-data-filtering

FastAPI already limits the output to the fields of the response model

Cited by