Python for JS Devs · FastAPI

Pydantic v2 models

Zod, but built into the framework. Define a model once and get validation, parsing, serialization, and docs — all from type hints.

Lesson 12Time ~13 min Win Validate a request body with constraintsPrereq Lessons 06, 07, 11

If you've used Zod or class-validator, Pydantic will feel instantly familiar — except it's the native currency of FastAPI. A BaseModel looks like a dataclass (Lesson 07) but validates at runtime.

1 · A model = a validated schema

Zod
const User = z.object({
  email: z.string().email(),
  name: z.string().min(2),
  age: z.number().optional(),
});
User.parse(data);
Pydantic v2
from pydantic import BaseModel, EmailStr, Field

class User(BaseModel):
    email: EmailStr
    name: str = Field(min_length=2)
    age: int | None = None

User(**data)   # validates & coerces, or raises ValidationError
Bug you already met Constraints live in Field(...), and the string one is min_length, not min. The AI-generated project you had wrote Field(min=5) — an invalid constraint that would blow up. Now you'll spot it instantly. (Numeric constraints are ge/le/gt/lt.)

2 · Wire it into FastAPI

Declare a model as a parameter and FastAPI reads it as the request body — parsing JSON, validating it, and handing you a typed object. No req.body, no manual checks.

@app.post("/users")
def create_user(user: User):   # body validated against User
    # `user` is a fully-typed, validated User instance here
    return {"created": user.email}

Send bad JSON (missing email, name too short) and FastAPI returns a precise 422 with a field-by-field error list — generated from the model.

3 · Parsing, serializing & coercion

u = User(email="a@b.co", name="kd", age="30")  # "30" coerced → 30
u.model_dump()          # → dict   (v1 was .dict())
u.model_dump_json()     # → JSON string
User.model_validate(raw)  # parse from an untrusted dict
v2 method names Pydantic v2 renamed things: .model_dump() / .model_dump_json() / .model_validate() (v1 used .dict() / .json() / .parse_obj()). If a tutorial uses the old names, it's Pydantic v1 — mentally translate.

🎯 Your win

Define the User model with name: str = Field(min_length=2) and an email: EmailStr (needs uv add "pydantic[email]"). Add the POST /users route, open /docs, and submit both a valid body and an invalid one (1-char name). Reading FastAPI's auto-generated 422 error is the win — you wrote no validation code.

Check yourself

Q1. The correct minimum-length constraint on a string field is:
It's Field(min_length=...). min is not a valid Pydantic constraint.
Q2. Declaring a Pydantic model as a handler param makes it the:
A model parameter is parsed and validated as the request body.
Q3. To turn a model instance into a dict in Pydantic v2 you call:
v2 uses .model_dump(); .dict() is the v1 name.
Primary source Pydantic — Models and Fields. The constraint list lives in Fields.
💬 Ask your teacher: how do I share fields between an input model and an output model without repeating myself? (Model inheritance — ask for the pattern.)