Guide
How to turn JSON Schema into Pydantic v2 models
A practical walkthrough for FastAPI teams: take a JSON Schema document (with $ref, required, oneOf), produce Pydantic v2 models, plug them straight into your routes.
Need it now? Skip ahead to the JSON Schema → Pydantic converter.
Step 1 — Have a JSON Schema document
You can use a standalone JSON Schema, or pull one out of your OpenAPI spec — anything under components.schemas.Xis already a JSON Schema. Here's a typical customer document with a $ref:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Customer",
"type": "object",
"required": ["id", "name", "address"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"address": { "$ref": "#/$defs/Address" }
},
"$defs": {
"Address": {
"type": "object",
"required": ["line1", "country"],
"properties": {
"line1": { "type": "string" },
"city": { "type": "string" },
"country": { "type": "string" }
}
}
}
}Step 2 — Generate Pydantic models
Paste the schema into the JSON Schema → Pydantic converter. The result resolves $defs into named classes, marks non-required fields as Optional[X] = None:
from __future__ import annotations
from typing import Any, Optional
from pydantic import BaseModel
class Address(BaseModel):
line1: str
city: Optional[str] = None
country: str
class Customer(BaseModel):
id: str
name: str
email: Optional[str] = None
address: AddressStep 3 — Plug into FastAPI
Drop the generated class into a route parameter. FastAPI uses Pydantic for parsing, validation, and OpenAPI documentation — all three for free:
from fastapi import FastAPI
from .models import Customer
app = FastAPI()
@app.post("/customers")
async def create_customer(customer: Customer) -> Customer:
# 'customer' is fully validated. Bad payloads return HTTP 422.
return customerA bad request body (missing fields, wrong types) returns HTTP 422 with a structured list of errors that points at exactly which field failed.
Step 4 — Refine with discriminators (oneOf)
JSON Schema's oneOf with a discriminator becomes a Pydantic discriminated union. The generator produces a generic Union — tighten it by hand:
from typing import Literal, Union
from pydantic import BaseModel, Field
class CardPayment(BaseModel):
kind: Literal["card"]
last4: str
class WalletPayment(BaseModel):
kind: Literal["wallet"]
provider: str
Payment = Union[CardPayment, WalletPayment]
class Order(BaseModel):
id: str
payment: Payment = Field(discriminator="kind")Step 5 — Decide on strictness
- • Default (lax): Pydantic coerces types —
"42"becomes42. Convenient for HTML forms, risky for typed APIs. - • Strict mode: set
model_config = ConfigDict(strict=True)on the class to reject coercion. Recommended for service-to-service contracts. - • Per-field strictness:
id: int = Field(..., strict=True)if you only want strict for some fields.
Common pitfalls
- • External $ref isn't resolved. Only same-document references are walked. Inline external schemas first or use a JSON Schema bundler.
- • String formats stay as plain str.
format: emaildoesn't auto-becomeEmailStr. Swap manually if you want runtime checks. - • Empty enum / arrays. Empty
enum: []or arrays withoutitemsdegrade toAny. - • allOf is shallow-merged. Properties and required arrays are merged; deeper composition (e.g. nested allOf inside a property) may need manual cleanup.
FAQ
Yes — same-document references in $defs / definitions / components.schemas become named classes.
They become Union types. Add a Field(discriminator=...) manually for tagged variants.
The generated code uses v2 idioms (list[X] type hints, model_config). Targets Python 3.10+.
Strings stay as str. Swap to EmailStr or datetime when you want stricter parsing.