JSON in FastAPI: Pydantic Models, Request Body, Response Schema & Validation

Last updated:

FastAPI serializes Pydantic BaseModel subclasses to JSON automatically — returning a model instance from a route function produces a Content-Type: application/json response with zero additional code. @app.post("/users", response_model=UserResponse) deserializes the JSON request body into a typed Pydantic model, validates it with Pydantic v2's Rust-powered validator (5–50× faster than v1), and serializes the return value using only the fields declared in UserResponse — preventing accidental data leaks. Field(alias="user_name") maps a JSON key to a Python attribute name; model_config = ConfigDict(populate_by_name=True) allows both names in input. FastAPI generates OpenAPI 3.1 docs at /docs and /openapi.json from these same Pydantic models with no extra work. This guide covers defining Pydantic models for JSON I/O, request body validation, response filtering with response_model, field aliases, nested models, JSONResponse for raw output, and handling validation errors.

Defining JSON Request Bodies with Pydantic BaseModel

Any class that inherits from Pydantic's BaseModel and is declared as a route parameter type becomes a JSON request body. FastAPI reads the request body, parses it as JSON, validates it against the model, and injects the typed model instance into your function. Field types use standard Python annotations: str, int, float, bool, list, dict, datetime, and UUID all work out of the box. Mark optional fields with Optional[T] or T | None and provide a default value.

from fastapi import FastAPI
from pydantic import BaseModel, Field, EmailStr
from typing import Optional
from datetime import datetime
import uuid

app = FastAPI()

# ── Define request body model ──────────────────────────────────
class CreateUserRequest(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    email: EmailStr                       # validated email format
    age: Optional[int] = Field(None, ge=0, le=150)
    bio: Optional[str] = None            # nullable + optional

# ── Route that accepts JSON body ───────────────────────────────
@app.post("/users")
async def create_user(user: CreateUserRequest):
    # FastAPI has already validated the JSON body
    # 'user' is a fully typed CreateUserRequest instance
    return {
        "id": str(uuid.uuid4()),
        "name": user.name,
        "email": user.email,
        "age": user.age,
        "bio": user.bio,
        "created_at": datetime.utcnow().isoformat(),
    }

# ── Multiple body params: wrap in a single model ───────────────
class UpdateUserRequest(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    bio: Optional[str] = None

@app.patch("/users/{user_id}")
async def update_user(user_id: str, body: UpdateUserRequest):
    # Only the fields sent in the JSON body are set
    # Fields not sent remain None (unset)
    updates = body.model_dump(exclude_unset=True)
    # updates = {"name": "Alice"} if only name was sent
    return {"user_id": user_id, "updated_fields": list(updates.keys())}

# ── Combining path/query params with a body ────────────────────
from fastapi import Query

@app.post("/orgs/{org_id}/members")
async def add_member(
    org_id: str,                             # path parameter
    role: str = Query(default="member"),     # query parameter
    member: CreateUserRequest = ...,         # request body
):
    return {"org_id": org_id, "role": role, "member": member.model_dump()}

FastAPI distinguishes request body parameters from path and query parameters by type: if a parameter type is a BaseModel subclass, it is treated as the JSON body. If it is a scalar type like str or int, FastAPI looks for it in the path first, then the query string. Use Body(...) from fastapi to force a scalar to be read from the body, or to embed multiple models under separate JSON keys. Field constraints from Field()min_length, max_length, ge, le, pattern — are enforced during validation and appear in the OpenAPI schema automatically.

response_model: Filtering JSON Output Fields

response_model is the most important FastAPI decorator parameter for JSON APIs. It declares what the response JSON should look like — and FastAPI enforces it by calling response_model.model_validate(return_value) before serialization. Fields in the return value that are not declared in response_model are silently dropped. This makes it safe to return a database ORM object with dozens of fields (including sensitive ones like hashed_password) and guarantee only the declared public fields reach the client.

from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

# ── Full internal DB model (has sensitive fields) ──────────────
class UserDB(BaseModel):
    id: str
    name: str
    email: str
    hashed_password: str   # ← must never be sent to clients
    is_admin: bool
    bio: Optional[str] = None

# ── Public response model (safe subset) ───────────────────────
class UserResponse(BaseModel):
    id: str
    name: str
    email: str
    bio: Optional[str] = None

# ── response_model filters output automatically ────────────────
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: str):
    # Simulated DB fetch — returns full UserDB with sensitive fields
    db_user = UserDB(
        id=user_id,
        name="Alice",
        email="alice@example.com",
        hashed_password="$2b$12$...",  # ← dropped by response_model
        is_admin=True,                  # ← dropped by response_model
        bio="Software engineer",
    )
    return db_user  # FastAPI validates against UserResponse before sending

# Response JSON: {"id":"...", "name":"Alice", "email":"alice@example.com", "bio":"..."}
# hashed_password and is_admin are never sent

# ── response_model_exclude_none ────────────────────────────────
@app.get(
    "/users/{user_id}/public",
    response_model=UserResponse,
    response_model_exclude_none=True,  # omit null fields
)
async def get_user_public(user_id: str):
    return UserResponse(id=user_id, name="Bob", email="bob@example.com")
# Response: {"id":"...", "name":"Bob", "email":"bob@example.com"}
# bio is omitted entirely (not sent as null)

# ── response_model_exclude_unset ───────────────────────────────
class ItemUpdate(BaseModel):
    name: Optional[str] = None
    price: Optional[float] = None
    in_stock: Optional[bool] = None

@app.patch(
    "/items/{item_id}",
    response_model=ItemUpdate,
    response_model_exclude_unset=True,
)
async def patch_item(item_id: str, update: ItemUpdate):
    # Only echo back the fields that were actually sent
    return update  # unset fields are excluded from the response

Use response_model_exclude_unset=True for PATCH endpoints to return only the fields the client actually sent. Use response_model_exclude_none=True to keep responses lean by omitting null fields. Both options are applied by FastAPI via model.model_dump(exclude_unset=True) and model.model_dump(exclude_none=True) respectively. You can combine them: response_model_exclude_none=True, response_model_exclude_unset=True omits both unset and explicitly-null fields.

Field Aliases and JSON Key Mapping

Python convention uses snake_case for attribute names, but JSON APIs often use camelCase or other naming conventions. Pydantic's Field(alias=...) maps JSON keys to Python attribute names. model_config = ConfigDict(populate_by_name=True) allows both the alias and the Python name in input, preventing breakage if your code uses the field name directly. For automatic camelCase alias generation across an entire model, use an alias_generator.

from fastapi import FastAPI
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional

app = FastAPI()

# ── Field-level alias ──────────────────────────────────────────
class CreatePostRequest(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    title: str
    body_text: str = Field(..., alias="bodyText")  # accepts "bodyText" in JSON
    author_id: str = Field(..., alias="authorId")

# Input JSON: {"title": "Hello", "bodyText": "...", "authorId": "u1"}
# With populate_by_name=True also accepts: {"title":"Hello","body_text":"...","author_id":"u1"}

@app.post("/posts")
async def create_post(post: CreatePostRequest):
    return {"title": post.title, "body": post.body_text, "author": post.author_id}

# ── Automatic camelCase alias generator ────────────────────────
# pip install pyhumps
from humps import to_camel  # converts snake_case → camelCase

class UserProfile(BaseModel):
    model_config = ConfigDict(
        alias_generator=to_camel,
        populate_by_name=True,  # also allow snake_case field names
    )

    first_name: str    # alias: "firstName"
    last_name: str     # alias: "lastName"
    date_of_birth: Optional[str] = None  # alias: "dateOfBirth"

# ── Emit camelCase in the response ────────────────────────────
@app.get("/profile/{user_id}", response_model=UserProfile)
async def get_profile(user_id: str):
    return UserProfile(first_name="Alice", last_name="Smith", date_of_birth="1990-01-15")

# Without response_model_by_alias=True, response uses snake_case field names
# With response_model_by_alias=True, response uses camelCase aliases

@app.get(
    "/profile/{user_id}/camel",
    response_model=UserProfile,
    response_model_by_alias=True,        # emit camelCase keys
)
async def get_profile_camel(user_id: str):
    return UserProfile(first_name="Alice", last_name="Smith")
# Response: {"firstName": "Alice", "lastName": "Smith"}

# ── Serialization alias (output only) ─────────────────────────
class PublicUser(BaseModel):
    internal_id: str = Field(..., serialization_alias="userId")  # output key
    display_name: str = Field(..., serialization_alias="name")

@app.get("/users/{uid}", response_model_by_alias=True)
async def get_public_user(uid: str):
    return PublicUser(internal_id=uid, display_name="Alice")
# Response: {"userId": "...", "name": "Alice"}

Separate alias (used for input validation) from serialization_alias (used for JSON output) when your API receives and emits different key names — for example, a legacy client sending user_id while the response returns userId. For new projects, pick one casing convention and apply it globally with alias_generator rather than per-field aliases, which become difficult to maintain at scale.

Nested Models and JSON Arrays

Pydantic models can be nested: a field whose type is another BaseModel subclass is validated recursively. Arrays of models use list[ItemModel]. Validation errors inside nested structures include the full path in the loc field — for example, ["body", "items", 0, "price"] — so clients can precisely identify which nested field failed. The OpenAPI schema reflects the nesting with $ref references.

from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime

app = FastAPI()

# ── Nested Pydantic models ─────────────────────────────────────
class Address(BaseModel):
    street: str
    city: str
    country: str = Field(..., min_length=2, max_length=2)  # ISO 2-letter

class OrderItem(BaseModel):
    product_id: str
    quantity: int = Field(..., ge=1)
    unit_price: float = Field(..., gt=0)

class CreateOrderRequest(BaseModel):
    customer_name: str
    shipping_address: Address         # nested model
    items: list[OrderItem]            # array of nested models
    notes: Optional[str] = None

# ── Route accepting nested JSON ────────────────────────────────
@app.post("/orders")
async def create_order(order: CreateOrderRequest):
    total = sum(item.quantity * item.unit_price for item in order.items)
    return {
        "order_id": "ord_001",
        "customer": order.customer_name,
        "city": order.shipping_address.city,
        "item_count": len(order.items),
        "total": round(total, 2),
    }

# Input JSON:
# {
#   "customer_name": "Alice",
#   "shipping_address": {"street": "123 Main St", "city": "NYC", "country": "US"},
#   "items": [
#     {"product_id": "p1", "quantity": 2, "unit_price": 9.99},
#     {"product_id": "p2", "quantity": 1, "unit_price": 29.99}
#   ]
# }

# ── Response model with nested types ──────────────────────────
class ProductSummary(BaseModel):
    id: str
    name: str
    price: float

class OrderResponse(BaseModel):
    order_id: str
    items: list[ProductSummary]
    total: float
    created_at: datetime

@app.get("/orders/{order_id}", response_model=OrderResponse)
async def get_order(order_id: str):
    return OrderResponse(
        order_id=order_id,
        items=[
            ProductSummary(id="p1", name="Widget", price=9.99),
            ProductSummary(id="p2", name="Gadget", price=29.99),
        ],
        total=49.97,
        created_at=datetime.utcnow(),  # serialized as ISO 8601 string
    )

# ── Returning a plain list ─────────────────────────────────────
@app.get("/products", response_model=list[ProductSummary])
async def list_products():
    return [
        ProductSummary(id="p1", name="Widget", price=9.99),
        ProductSummary(id="p2", name="Gadget", price=29.99),
    ]
# Response: [{"id":"p1","name":"Widget","price":9.99}, ...]

For ORM integration, set model_config = ConfigDict(from_attributes=True) on your response models (the Pydantic v2 equivalent of the old orm_mode = True). This allows Pydantic to read attributes from SQLAlchemy ORM objects directly rather than requiring dict conversion. FastAPI calls model_validate(orm_obj) with from_attributes=True automatically when the response model has this config, enabling clean return db_row patterns.

Validation Errors: 422 Unprocessable Entity Response Format

When Pydantic validation fails on a request body, FastAPI returns HTTP 422 with a structured JSON body. Each validation error is represented in the detail array. The format is consistent and machine-readable — clients can parse it programmatically to map errors back to form fields. You can customize this format globally with an exception handler, or per-route by catching RequestValidationError from fastapi.exceptions.

from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, EmailStr
from typing import Optional

app = FastAPI()

class SignupRequest(BaseModel):
    username: str = Field(..., min_length=3, max_length=20)
    email: EmailStr
    age: int = Field(..., ge=18, le=120)
    password: str = Field(..., min_length=8)

@app.post("/signup")
async def signup(body: SignupRequest):
    return {"message": "User created", "username": body.username}

# ── Default 422 response format ────────────────────────────────
# POST /signup with body: {"username": "ab", "email": "bad", "age": 15}
# HTTP 422 Unprocessable Entity
# {
#   "detail": [
#     {
#       "type": "string_too_short",
#       "loc": ["body", "username"],
#       "msg": "String should have at least 3 characters",
#       "input": "ab",
#       "ctx": {"min_length": 3}
#     },
#     {
#       "type": "value_error",
#       "loc": ["body", "email"],
#       "msg": "value is not a valid email address",
#       "input": "bad"
#     },
#     {
#       "type": "greater_than_equal",
#       "loc": ["body", "age"],
#       "msg": "Input should be greater than or equal to 18",
#       "input": 15,
#       "ctx": {"ge": 18}
#     },
#     {
#       "type": "missing",
#       "loc": ["body", "password"],
#       "msg": "Field required"
#     }
#   ]
# }

# ── Custom 422 handler — transform to your API's error format ──
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
    request: Request,
    exc: RequestValidationError,
) -> JSONResponse:
    errors = {}
    for error in exc.errors():
        # loc is e.g. ("body", "username") or ("body", "items", 0, "price")
        field = ".".join(str(p) for p in error["loc"][1:])  # skip "body"
        errors[field] = error["msg"]

    return JSONResponse(
        status_code=422,
        content={
            "success": False,
            "errors": errors,
        },
    )
# Custom response: {"success": false, "errors": {"username": "String should have..."}}

# ── Raising validation errors manually from route logic ────────
from fastapi import HTTPException

@app.post("/login")
async def login(body: SignupRequest):
    if body.username == "reserved":
        raise HTTPException(status_code=422, detail=[{
            "loc": ["body", "username"],
            "msg": "Username is reserved",
            "type": "value_error",
        }])
    return {"token": "..."}

The loc array is a path from the root of the request to the failing field. For nested models, it includes intermediate key names and array indices: ["body", "items", 0, "unit_price"] means the first element of the items array failed on its unit_price field. Client-side code can use this path to highlight the exact form field that failed. The type field is a machine-readable Pydantic error code — use it for programmatic error routing rather than parsing msg strings, which can change between Pydantic versions.

JSONResponse and ORJSONResponse for Raw JSON Output

For cases where you need to return JSON without Pydantic validation — pre-built dicts, cached JSON strings, or non-model data — use JSONResponse directly. For high-throughput endpoints, ORJSONResponse from fastapi.responses uses the orjson library, which is typically 2–3× faster than Python's built-in json module and natively handles datetime, UUID, numpy arrays, and dataclasses without custom encoders.

from fastapi import FastAPI
from fastapi.responses import JSONResponse, ORJSONResponse, StreamingResponse
from pydantic import BaseModel
from datetime import datetime
import uuid
import asyncio
import json

app = FastAPI()

# ── JSONResponse — bypass Pydantic, return raw dict ───────────
@app.get("/health")
async def health_check():
    return JSONResponse(content={
        "status": "ok",
        "version": "1.2.3",
        "timestamp": datetime.utcnow().isoformat(),  # must serialize manually
    })

# ── JSONResponse with custom status code and headers ──────────
@app.post("/items")
async def create_item(name: str):
    return JSONResponse(
        content={"id": str(uuid.uuid4()), "name": name},
        status_code=201,
        headers={"X-Item-Id": "new-item"},
    )

# ── ORJSONResponse — 2–3× faster, handles datetime/UUID natively
# pip install orjson
@app.get("/metrics", response_class=ORJSONResponse)
async def get_metrics():
    return {  # ORJSONResponse serializes this dict directly
        "requests_total": 1_234_567,
        "last_request": datetime.utcnow(),  # orjson handles datetime natively
        "server_id": uuid.uuid4(),           # orjson handles UUID natively
    }

# ── Default response class for the entire app ─────────────────
app_orjson = FastAPI(default_response_class=ORJSONResponse)

@app_orjson.get("/data")
async def get_data():
    return {"key": "value"}  # always uses ORJSONResponse

# ── Streaming JSON (NDJSON) for large datasets ─────────────────
async def generate_records():
    for i in range(1000):
        record = {"id": i, "name": f"item_{i}", "ts": datetime.utcnow().isoformat()}
        yield json.dumps(record) + "
"
        await asyncio.sleep(0)  # yield control to event loop

@app.get("/stream")
async def stream_records():
    return StreamingResponse(
        generate_records(),
        media_type="application/x-ndjson",
    )

# ── Return a Pydantic model from a JSONResponse ───────────────
class Item(BaseModel):
    id: str
    name: str

@app.get("/items/{item_id}")
async def get_item(item_id: str):
    item = Item(id=item_id, name="Widget")
    # model_dump() converts to dict, then JSONResponse serializes
    return JSONResponse(content=item.model_dump())

Use ORJSONResponse as the default_response_class for your entire application when latency matters — the serialization speedup is free if you already return plain dicts or Pydantic models with model_dump(). ORJSONResponse handles Python's built-in datetime, UUID, and bytes without custom encoders. Note that orjson is strict about types — it will raise TypeError for objects it cannot serialize, whereas Python's json.dumps raises a more generic TypeError. Test your responses after switching.

Auto-Generated OpenAPI JSON from Pydantic Models

FastAPI builds the OpenAPI 3.1 JSON document from your route decorators and Pydantic models at startup, with no additional code. Every BaseModel used as a request body or response_model becomes a named schema in the components/schemas section, referenced with $ref throughout the document. Field descriptions from Field(description=...), examples from Field(examples=[...]), and constraints from Field(ge=..., le=...) all appear in the schema. The document is available at /openapi.json and the interactive Swagger UI at /docs.

from fastapi import FastAPI
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
import json

app = FastAPI(
    title="My API",
    description="API for managing users and orders",
    version="2.0.0",
    openapi_url="/openapi.json",   # default, can change or disable
    docs_url="/docs",              # Swagger UI
    redoc_url="/redoc",            # ReDoc
)

# ── Models with rich field metadata ───────────────────────────
class UserCreateRequest(BaseModel):
    name: str = Field(
        ...,
        description="Full display name",
        min_length=1,
        max_length=100,
        examples=["Alice Smith"],
    )
    email: str = Field(
        ...,
        description="Contact email address",
        examples=["alice@example.com"],
    )
    role: str = Field(
        default="user",
        description="User role",
        pattern="^(user|admin|moderator)$",
    )

class UserResponse(BaseModel):
    model_config = ConfigDict(
        json_schema_extra={
            "example": {
                "id": "usr_123",
                "name": "Alice Smith",
                "email": "alice@example.com",
                "role": "user",
            }
        }
    )

    id: str
    name: str
    email: str
    role: str

@app.post(
    "/users",
    response_model=UserResponse,
    summary="Create a user",
    description="Creates a new user account. Returns the created user.",
    tags=["users"],
    status_code=201,
    responses={
        201: {"description": "User created successfully"},
        422: {"description": "Validation error"},
    },
)
async def create_user(body: UserCreateRequest):
    return UserResponse(id="usr_123", **body.model_dump())

# ── Access the OpenAPI JSON programmatically ───────────────────
@app.get("/schema-export", include_in_schema=False)
async def export_schema():
    schema = app.openapi()
    return schema  # returns the full OpenAPI dict

# ── Customize the OpenAPI schema ──────────────────────────────
def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema

    from fastapi.openapi.utils import get_openapi
    schema = get_openapi(
        title=app.title,
        version=app.version,
        routes=app.routes,
    )
    # Add custom x-logo extension
    schema["info"]["x-logo"] = {"url": "https://example.com/logo.png"}
    app.openapi_schema = schema
    return schema

app.openapi = custom_openapi  # type: ignore

# ── Disable docs in production ─────────────────────────────────
import os
if os.getenv("ENV") == "production":
    app_prod = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)

The /openapi.json endpoint returns the complete OpenAPI 3.1 document as JSON. You can use this to generate TypeScript clients with openapi-generator-cli, Python clients with openapi-python-client, or documentation with Redoc. The schema is cached after the first request — FastAPI does not regenerate it on every request. To force regeneration (e.g., after dynamic route registration), set app.openapi_schema = None before the next request.

Key Terms

BaseModel
pydantic.BaseModel is the base class for all Pydantic data models. Subclasses declare fields as class attributes with Python type annotations. Pydantic validates field values on model creation — raising ValidationError if any field fails — and serializes models to JSON via model_dump() and model_dump_json(). In FastAPI, any route parameter annotated with a BaseModel subclass type becomes a JSON request body, parsed and validated automatically from the HTTP request. Pydantic v2 uses a Rust-based validation core, making validation 5–50× faster than Pydantic v1. The model_config class variable (a ConfigDict instance) controls serialization behavior: from_attributes enables ORM mode, populate_by_name allows field names alongside aliases, and alias_generator automates alias creation.
response_model
The response_model parameter on FastAPI route decorators (@app.get, @app.post, etc.) declares the Pydantic model used to validate and serialize the JSON response. FastAPI calls response_model.model_validate(return_value) after the route function returns, then serializes the resulting model instance to JSON. Fields present in the return value but absent from response_model are silently excluded — this is the primary mechanism for preventing sensitive data leaks. response_model also drives OpenAPI schema generation: FastAPI adds the model's JSON Schema to components/schemas and references it in the route's response definition. Use response_model=None to disable response validation for a specific route.
Field alias
A field alias is an alternative name for a Pydantic model field used in JSON serialization and deserialization. Declared with Field(alias="json_key"), it maps a JSON key to a differently-named Python attribute. Without ConfigDict(populate_by_name=True), only the alias is accepted in JSON input, not the field name. Pydantic v2 separates validation aliases (for input) from serialization aliases (for output): Field(validation_alias="input_key", serialization_alias="output_key"). An alias_generator in model_config applies a function to every field name to generate aliases automatically — useful for globally converting snake_case Python names to camelCase JSON keys without per-field annotation.
JSONResponse
fastapi.responses.JSONResponse is a Starlette response class that serializes a Python dict or list to JSON and sets Content-Type: application/json. It bypasses Pydantic validation — the content argument is passed directly to json.dumps(). Use JSONResponse when you need precise control over the response (custom status codes, headers) or when returning pre-built data structures that do not map to a Pydantic model. ORJSONResponse is a drop-in replacement using the orjson library for 2–3× faster serialization with native support for datetime, UUID, and bytes. UJSONResponse uses ujson as an alternative fast serializer.
422 Unprocessable Entity
HTTP 422 is the status code FastAPI returns when Pydantic validation fails on a request body. The response body has a detail array where each element is a validation error object with loc (field path array), msg (human-readable description), type (machine-readable Pydantic error code), and optionally ctx (constraint context like {"min_length": 3}). FastAPI generates the 422 response automatically for all request body validation failures without any code in the route handler. Custom 422 formats can be implemented via app.add_exception_handler(RequestValidationError, handler). The 422 schema is automatically documented in the OpenAPI output under each route's response definitions.
ConfigDict
pydantic.ConfigDict is the Pydantic v2 configuration mechanism for BaseModel subclasses, replacing the class Config inner class from Pydantic v1. Set it as a class variable: model_config = ConfigDict(...). Key options include: from_attributes=True (read from ORM object attributes, replaces orm_mode), populate_by_name=True (accept field names alongside aliases), alias_generator (function to auto-generate aliases), extra="forbid" (reject unknown fields in input), str_strip_whitespace=True (auto-strip string whitespace), and json_schema_extra (add custom keys to the generated JSON Schema for OpenAPI documentation).

FAQ

How does FastAPI serialize Python objects to JSON?

FastAPI serializes Python objects to JSON through Pydantic v2's model serializer. When you return a BaseModel instance, FastAPI calls model.model_dump() internally, then passes the resulting dict to Python's JSON encoder via Pydantic's serialization layer — producing a Content-Type: application/json response automatically. Pydantic v2's Rust-powered core makes this 5–50× faster than Pydantic v1 for the validation step. FastAPI also handles standard Python types: returning a plain dict, list, int, or str serializes directly. For non-JSON-serializable types like datetime and UUID, Pydantic handles encoding automatically when they appear inside a model. If you return a bare datetime without a response model, use JSONResponse or a Pydantic model wrapper to control encoding.

What is response_model in FastAPI and why should I use it?

response_model is a route decorator parameter that declares the Pydantic model used to serialize and filter the JSON response. Its primary purpose is output filtering: FastAPI calls response_model.model_validate(return_value) and serializes only the fields declared in that model — even if your route returns a larger ORM object or dict with extra keys. This prevents accidental data leaks such as returning a hashed_password field when the client should only see a public user profile. response_model also drives the OpenAPI schema: FastAPI generates the response schema at /openapi.json from the model automatically. Use response_model=list[UserResponse] for array endpoints. Set response_model_exclude_none=True to omit None fields and reduce payload size. In FastAPI 0.100+ with Pydantic v2, response model validation uses model_validate() which is significantly faster than v1.

How do I handle JSON validation errors in FastAPI?

When a request body fails Pydantic validation, FastAPI automatically returns HTTP 422 Unprocessable Entity with a JSON body: {"detail": [{"loc": [...], "msg": "...", "type": "..."}]}. Each element in the detail array corresponds to one validation failure. The loc field is an array like ["body", "email"] showing the path to the failing field. You do not need to write any try/except code — FastAPI handles this automatically. To customize the 422 response format, add an exception handler: app.add_exception_handler(RequestValidationError, custom_handler). To handle validation errors in your own route logic, catch pydantic.ValidationError explicitly when calling model_validate() directly. The 422 response schema is documented in /openapi.json automatically for every route that has a request body.

How do I use field aliases in FastAPI JSON models?

Use Field(alias="json_key_name") to map a JSON key to a differently-named Python attribute. For example, Field(alias="firstName") makes the JSON input accept "firstName" while the Python attribute is named first_name. By default in Pydantic v2, only the alias is accepted in JSON input. To allow both the alias and the field name, set model_config = ConfigDict(populate_by_name=True). For automatic camelCase conversion across an entire model, use ConfigDict(alias_generator=to_camel) from the humps library (pip install pyhumps). For serialization output, use Field(serialization_alias="output_key") to control what key appears in the JSON response. To emit aliased keys in the response, set response_model_by_alias=True in the route decorator.

How do I return a list of JSON objects from a FastAPI route?

Set response_model=list[UserResponse] (Python 3.9+) or response_model=List[UserResponse] in the route decorator, then return a list of Pydantic model instances or dicts. FastAPI validates and serializes each element against UserResponse. For database results, return a list directly: return db.query(User).all() — FastAPI calls model_validate on each ORM object via from_attributes=True. For large lists, use response_model_exclude_none=True to trim None fields and reduce payload. To add pagination metadata, wrap in an envelope model: class PagedResponse(BaseModel): items: list[UserResponse]; total: int. For streaming very large lists (thousands of items), use StreamingResponse with an async generator that yields NDJSON lines instead of building the full list in memory.

How do I exclude None values from FastAPI JSON responses?

Set response_model_exclude_none=True in the route decorator: @app.get("/users/{id}", response_model=UserResponse, response_model_exclude_none=True). FastAPI passes exclude_none=True to model.model_dump() before serialization, omitting all fields whose value is None. This is the standard pattern for optional JSON fields in REST APIs, keeping responses lean by avoiding {"bio": null, "website": null} noise. Use response_model_exclude_unset=True instead to omit fields that were never assigned (as opposed to fields explicitly set to None) — useful for PATCH endpoints. Combining both omits all fields that are either unset or None, producing the most compact response possible.

How does FastAPI generate OpenAPI JSON automatically?

FastAPI generates OpenAPI 3.1 JSON by introspecting route decorators and Pydantic models at startup. Every route decorator contributes a path item to /openapi.json. The response_model parameter becomes the response schema under components/schemas. Request body Pydantic models become requestBody schemas. FastAPI uses Pydantic v2's model_json_schema() to generate JSON Schema from each model. The OpenAPI document is available at /openapi.json and the Swagger UI at /docs. To add examples, use Field(examples=[...]) or model_config = ConfigDict(json_schema_extra={"example": {...}}). To disable docs in production, initialize with FastAPI(docs_url=None, redoc_url=None, openapi_url=None). The schema is cached after the first request — FastAPI does not regenerate it on every call.

How do I accept both camelCase and snake_case JSON in FastAPI?

The cleanest approach uses Pydantic's alias_generator with populate_by_name=True. Install humps (pip install pyhumps) and set model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) on your model. This generates camelCase aliases for all fields automatically: first_name gets alias "firstName". With populate_by_name=True, both "firstName" and "first_name" are accepted in JSON input. For output, add response_model_by_alias=True to the route to emit camelCase keys in the response. If you only need a few fields, use per-field Field(alias="camelCase"). For a single API, choose one convention and apply it globally — mixing camelCase and snake_case in the same endpoint creates maintenance burden and surprises for API consumers.

Generate Pydantic models from JSON automatically

Paste any JSON object into Jsonic's generator to get a complete Pydantic v2 model instantly — no manual typing required.

Open JSON to Pydantic Generator

Further reading and primary sources