# How Pydantic Validates Request and Response Data Schemas in Open Notebook

> Discover how Open Notebook leverages Pydantic v2 for robust request and response data schema validation. FastAPI ensures type-safe API contracts with Pydantic models.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: how-to-guide
- Published: 2026-06-06

---

**Open Notebook uses Pydantic v2 models in [`api/models.py`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/api/models.py).

Required fields use the ellipsis sentinel to force presence:

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

```

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

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

```

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

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

```

Range constraints clamp numeric inputs to valid intervals:

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

```

Literal types restrict inputs to allowed enum strings:

```python
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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/api/models.py), the `TransformationExecuteRequest` model disables Pydantic's reserved-name protection with:

```python
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`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/notebooks.py) router consumes the `NotebookCreate` schema directly. By the time the endpoint body executes, the payload is a fully validated instance:

```python

# 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`](https://github.com/lfnovo/open-notebook/blob/main/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:

```python
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`](https://github.com/lfnovo/open-notebook/blob/main/api/models.py). Internal domain objects extend the same base machinery in [`open_notebook/domain/base.py`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/base.py) and [`open_notebook/graphs/ask.py`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/base.py) through `ObjectModel` and `RecordModel`, both extending `BaseModel`. The LangGraph pipeline in [`open_notebook/graphs/ask.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/ask.py) also uses Pydantic-based parsing to validate structured outputs from AI workflows.