Pydantic Models and Validators

RoadmapsPython Backend

Scenario

Your API accepts a 'discount_percentage' field. A user sends 'discount_percentage': 150. Your database accepts it, and suddenly every order is completely free.

How do you guarantee that bad data never even reaches your business logic?

Mental model

Pydantic is a ruthless bouncer at the club door.

It checks everyone's ID (data types) and applies the club rules (validators). If the payload says age=17, the bouncer rejects them with a 422 Unprocessable Entity before they even step inside your FastAPI route. Your internal code never has to worry about checking types or rules again.

Deep dive

In a backend, you can't trust the data the client sends. Instead of scattering `if age < 0` checks throughout your handlers, you declare a Pydantic model. You state exactly what the data should look like using standard Python type hints. Pydantic enforces this boundary.

Models define the boundary: bad data bounces before your logic runs.

Pydantic does two things: parsing and validation. Parsing means if you expect an integer and receive the string `"42"`, Pydantic silently converts it to the integer `42` (coercion). Validation means if you expect an integer and receive `"apple"`, Pydantic raises a clear validation error detailing exactly which field failed.

Parsing coerces types when possible; validation rejects what can't be coerced.

Standard types aren't always enough. A string could be empty, or an integer might be out of range. Pydantic v2 uses `Field` constraints (`min_length`, `ge` for greater-than-or-equal) and the `@field_validator` decorator to enforce custom business rules directly on the data shape.

Use Field for simple bounds and @field_validator for custom logic.

When a Pydantic v2 model fails validation inside FastAPI, the framework automatically catches the `ValidationError` and returns a 422 HTTP response to the client. The response body is a highly detailed JSON array explaining every single field that failed and why. You never write this error handling code yourself.

FastAPI + Pydantic automatically turns validation failures into 422 responses.

Code examples

The naive way (no Pydantic)

def create_user(payload: dict):
    # Business logic polluted with validation
    if "age" not in payload:
        raise ValueError("age is required")
    if not isinstance(payload["age"], int):
        raise ValueError("age must be int")
    if payload["age"] < 18:
        raise ValueError("must be 18 or older")
    
    return save_to_db(payload)

Without a model, you have to manually check for missing keys, wrong types, and business rules. It scales poorly and makes the handler unreadable.

The realistic version (Pydantic v2)

from pydantic import BaseModel, Field, field_validator

class UserCreate(BaseModel):
    username: str = Field(min_length=3, max_length=50)
    age: int = Field(ge=18)  # greater than or equal to 18
    email: str

    @field_validator('email')
    @classmethod
    def must_be_company_email(cls, v: str) -> str:
        if not v.endswith('@company.com'):
            raise ValueError('Must use a company email address')
        return v

Here, Pydantic v2's `Field` handles simple constraints, while `@field_validator` handles custom logic. The classmethod takes the raw value and either returns it or raises an error.

The modern v2 configuration pattern

from pydantic import BaseModel, ConfigDict

class UserResponse(BaseModel):
    # v2 configuration replaces the old `class Config:`
    model_config = ConfigDict(from_attributes=True, extra='forbid')

    id: int
    username: str
    email: str

In Pydantic v2, configurations use `model_config = ConfigDict(...)`. The `from_attributes=True` setting is critical for serializers, allowing Pydantic to read fields from SQLAlchemy ORM objects (e.g. `user.id`), not just dictionaries.

Key points

Common mistakes

Recall questions

Questions & answers

A candidate's code review submission includes: `class User(BaseModel): class Config: orm_mode = True`. What is wrong with this in a modern FastAPI application?

It uses Pydantic v1 syntax. In modern FastAPI (Pydantic v2), configurations must use `model_config = ConfigDict(from_attributes=True)`.

Approach: Identify the outdated `class Config` and `orm_mode` syntax, and provide the correct v2 `ConfigDict` equivalent.

You have a Pydantic model for user registration. How do you enforce that the 'password' and 'confirm_password' fields match?

Use a `@model_validator(mode='after')` (or `mode='before'`) to inspect the entire model's data. A single `@field_validator` only has access to its own field, not the others.

Approach: Explain that field validators validate single fields in isolation, whereas a model validator has access to the full payload.

Continue learning

Previous: Request Body & response_model

Next: ConfigDict & from_attributes

Return to Python Backend Roadmap