From a JSON object to a BaseModel class
A Pydantic model is a subclass of BaseModel whose fields are plain annotated class attributes — no decorators or registration. The generator reads one JSON object and emits one such class, mapping each key to a typed field. This gives you parsing, coercion, and validation for free the moment you call a parse method on the class.
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"is_active": true
}from pydantic import BaseModel
class Model(BaseModel):
id: int
name: str
email: str
is_active: boolHow JSON values become Python types
The generator inspects each value and picks a type. Numbers are the subtle case: 42 becomes int while 9.99 becomes float, decided by the presence of a decimal point. JSON has no date or UUID type, so an ISO timestamp arrives as str — promote it to datetime yourself if you want Pydantic to coerce and validate it.
{
"sku": "WIDGET-1",
"quantity": 3,
"unit_price": 9.99,
"in_stock": true,
"released_at": "2026-01-01T00:00:00Z"
}from pydantic import BaseModel
class Model(BaseModel):
sku: str
quantity: int # no decimal -> int
unit_price: float # decimal point -> float
in_stock: bool
released_at: str # JSON has no date type; widen to datetime by handOptional fields and defaults from null
A null value produces Optional[T] with a default of None. The default matters: Optional[str] alone only means the value may be null — without = None Pydantic still requires the key to be present. For collection defaults, hand-edit to Field(default_factory=list) to dodge the shared-mutable-default trap.
{
"id": 1,
"name": "Alice",
"middle_name": null,
"tags": []
}from pydantic import BaseModel, Field
from typing import Optional
class Model(BaseModel):
id: int
name: str
middle_name: Optional[str] = None # null -> Optional + default
tags: list[str] = Field(default_factory=list) # safe mutable defaultNested models and List[Model]
Each nested JSON object becomes its own BaseModel subclass; an array of objects becomes List[ItemModel] plus a class for the element. Nested classes are declared before the models that reference them so Python resolves the names directly. This recurses to any depth.
{
"order_id": "ORD-001",
"customer": { "id": 42, "name": "Alice" },
"items": [
{ "product": "Widget", "quantity": 3 },
{ "product": "Gadget", "quantity": 1 }
]
}from pydantic import BaseModel
from typing import List
class Customer(BaseModel):
id: int
name: str
class Item(BaseModel):
product: str
quantity: int
class Model(BaseModel):
order_id: str
customer: Customer # nested object -> nested model
items: List[Item] # array of objects -> List[Item]Parse and validate JSON with the generated model
Once you have the class, model_validate_json() parses a raw JSON string into a validated instance in a single step — it runs through pydantic-core's Rust parser, so there is no separate json.loads() call. Use model_validate() when you already hold a Python dict, and TypeAdapter for a top-level array. Any failure raises a single ValidationError that lists every bad field at once.
from pydantic import BaseModel, ValidationError, TypeAdapter
class Model(BaseModel):
id: int
name: str
# 1) Raw JSON string -> validated model (preferred)
m = Model.model_validate_json('{"id": 1, "name": "Alice"}')
# 2) Already a Python dict
m2 = Model.model_validate({"id": 2, "name": "Bob"})
# 3) Top-level JSON array
users = TypeAdapter(list[Model]).validate_json('[{"id":1,"name":"A"},{"id":2,"name":"B"}]')
# 4) Structured errors instead of silent bad data
try:
Model.model_validate_json('{"id": "oops"}')
except ValidationError as e:
print(e.error_count(), "errors") # missing name + bad idAliases, snake_case keys, and v2 vs v1
camelCase keys are emitted as snake_case to match PEP 8 (userId → user_id). To keep accepting the original wire names, add a Field(alias=...) plus populate_by_name=True. On versions: the class bodies are identical across Pydantic v1 and v2 — what changed are the method names. In v2, model_validate_json() replaces parse_raw(), model_dump() replaces dict(), and ConfigDict replaces the inner Config class, all backed by a Rust core that is 5–50× faster.
from pydantic import BaseModel, Field, ConfigDict
class Model(BaseModel):
model_config = ConfigDict(populate_by_name=True) # accept both names
user_id: int = Field(alias="userId") # camelCase wire name
first_name: str = Field(alias="firstName")
# accepts the original API payload...
Model.model_validate_json('{"userId": 1, "firstName": "Alice"}')
# ...and the pythonic form
Model(user_id=1, first_name="Alice")For the deeper walkthrough — ValidationError handling, JSON Schema export, and FastAPI integration — read the JSON to Pydantic guide. If you target a TypeScript codebase instead of Python, use JSON to TypeScript.