How Pydantic Validates Request and Response Data Schemas in Open Notebook

Open Notebook uses Pydantic v2 models in api/models.py to automatically parse, coerce, and validate every inbound request, while FastAPI serializes outgoing responses against the same schemas to guarantee type-safe API contracts.

Open Notebook's FastAPI server relies on Pydantic to enforce data contracts between clients and the backend. Every request body and response shape is declared in api/models.py as a class extending pydantic.BaseModel, decorated with field constraints and custom validators. This article breaks down exactly how Pydantic validates request and response data schemas, from JSON parsing to error handling and automatic serialization.

How FastAPI Validates Incoming Requests with Pydantic v2

When a client sends a request to an Open Notebook endpoint, FastAPI binds the incoming payload to the Pydantic model declared in the route signature. The validation process proceeds through three stages before any business logic runs:

  1. Parse the incoming JSON (or URL-encoded form) into the Pydantic model class defined in the router's endpoint signature.

  2. Instantiate the model – Pydantic performs type coercion, enforces constraints defined in Field, and executes custom validators.

  3. Raise a ValidationError if any check fails; FastAPI automatically converts this into a 422 Unprocessable Entity response with a detailed error payload.

Type Coercion and Field Constraints

During model instantiation, Pydantic converts compatible types—for example, strings to int or ISO timestamps to datetime. It then evaluates constraints declared in Field(...) metadata in api/models.py.

Required fields use the ellipsis sentinel to force presence:

name: str = Field(..., description="Name of the notebook")

Optional fields accept missing or null values by defaulting to None:

description: Optional[str] = Field(None, description="Description of the notebook")

Value constraints protect endpoints from out-of-range input:

limit: int = Field(100, description="Maximum number of results", le=1000)

Range constraints clamp numeric inputs to valid intervals:

minimum_score: float = Field(0.2, description="Minimum score", ge=0, le=1)

Literal types restrict inputs to allowed enum strings:

type: Literal["text", "vector"] = Field("text")

If any check fails, Pydantic raises a ValidationError and FastAPI returns a detailed 422 Unprocessable Entity payload.

Custom Field Validation with field_validator

Beyond built-in constraints, api/models.py enforces business rules via @field_validator. These decorators run against a single field after initial parsing. For example, a validator on the api_key field ensures stored secrets are never empty strings by converting them to None or raising an error.

This pattern prevents malformed data from reaching downstream services without cluttering endpoint code with manual checks.

Model-Wide Validation with model_validator

For rules that depend on multiple fields, api/models.py uses @model_validator(mode="after"). The SourceCreate model demonstrates this by running post-build validation to ensure that only one of notebook_id or notebooks is supplied, and then normalizing the data before the instance is returned.

Because these validators execute after all fields are parsed, they can enforce cross-field logic that raw type hints cannot express.

Guaranteeing Response Schema Consistency with Pydantic

Pydantic's role does not end when validation succeeds. After an endpoint returns a model instance, FastAPI calls model.json() (or model.dict()) to produce the JSON response, applying any ConfigDict settings such as alias handling or field exclusion.

This guarantees that every outgoing payload matches the declared schema. In api/models.py, the TransformationExecuteRequest model disables Pydantic's reserved-name protection with:

model_config = ConfigDict(protected_namespaces=())

This allows field names that would otherwise be blocked by default namespace guards, ensuring the API contract remains flexible.

End-to-End Validation Examples in Open Notebook

Validating a NotebookCreate Request in api/routers/notebooks.py

The api/routers/notebooks.py router consumes the NotebookCreate schema directly. By the time the endpoint body executes, the payload is a fully validated instance:


# api/routers/notebooks.py

from fastapi import APIRouter, HTTPException
from api.models import NotebookCreate, NotebookResponse

router = APIRouter()

@router.post("/", response_model=NotebookResponse)
async def create_notebook(payload: NotebookCreate):
    # At this point `payload` is a validated NotebookCreate instance.

    if len(payload.name) < 3:
        raise HTTPException(status_code=400, detail="Name too short")
    # Business logic …

    return NotebookResponse(
        id="nb_123",
        name=payload.name,
        description=payload.description or "",
        archived=False,
        created="2024-01-01T00:00:00Z",
        updated="2024-01-01T00:00:00Z",
        source_count=0,
        note_count=0,
    )

Because FastAPI binds payload: NotebookCreate, the framework automatically rejects malformed or missing fields with a 422 response before the if statement is ever evaluated.

Serializing a SearchResponse in api/routers/search.py

The api/routers/search.py file uses SearchRequest and SearchResponse to handle queries. If a client sends { "query": "AI", "type": "image" }, Pydantic raises a validation error because "image" is not an allowed Literal["text", "vector"]; FastAPI returns 422 with a clear error message.

For valid requests, the response model is serialized automatically:

from fastapi import FastAPI
from api.models import SearchRequest, SearchResponse

app = FastAPI()

@app.post("/search", response_model=SearchResponse)
async def search(req: SearchRequest):
    # `req` is already validated.

    results = await perform_search(req.query, req.type, req.limit)
    return SearchResponse(
        results=results,
        total_count=len(results),
        search_type=req.type,
    )

This pattern keeps routers thin and guarantees that clients receive predictable JSON shapes.

Pydantic Validation Beyond the API Layer

Pydantic validation is not limited to api/models.py. Internal domain objects extend the same base machinery in open_notebook/domain/base.py, where ObjectModel and RecordModel inherit from BaseModel to provide a shared validation foundation for backend entities.

Additionally, open_notebook/graphs/ask.py leverages a Pydantic-based OutputParser inside LangGraph workflows, reusing the same validation machinery to enforce structured outputs from language model generations.

Summary

  • Pydantic v2 underpins every request and response contract in Open Notebook through api/models.py.
  • Type coercion, Field constraints, and literal validation catch malformed data before it enters endpoint logic.
  • @field_validator and @model_validator(mode="after") enforce custom business rules ranging from empty-string checks to cross-field dependencies.
  • FastAPI translates any ValidationError into a 422 Unprocessable Entity response with detailed diagnostics.
  • Response models are automatically serialized to JSON, ensuring clients receive consistent, schema-compliant payloads.
  • Internal files such as open_notebook/domain/base.py and open_notebook/graphs/ask.py extend the same validation foundation beyond the API surface.

Frequently Asked Questions

How does FastAPI use Pydantic models for request validation?

FastAPI inspects the type hints in endpoint signatures—such as payload: NotebookCreate—and automatically instantiates the corresponding Pydantic model from the incoming JSON body. Pydantic then parses, coerces, and validates the data; if validation passes, the endpoint receives a populated instance, otherwise FastAPI returns a 422 error.

What happens when a client sends a payload that violates a Field constraint?

Pydantic raises a ValidationError listing every violated constraint. FastAPI catches this exception and returns an HTTP 422 Unprocessable Entity response that includes a detailed error payload describing which fields failed and why.

When should I use field_validator versus model_validator in Pydantic v2?

Use @field_validator when a rule applies to a single field in isolation, such as stripping empty strings from an api_key. Use @model_validator(mode="after") when validation depends on multiple fields or requires the fully constructed model, as seen in SourceCreate where notebook_id and notebooks are checked against each other.

Where does Open Notebook define its internal domain models outside of api/models.py?

Shared internal models live in open_notebook/domain/base.py through ObjectModel and RecordModel, both extending BaseModel. The LangGraph pipeline in open_notebook/graphs/ask.py also uses Pydantic-based parsing to validate structured outputs from AI workflows.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →