Free & open source — no account required
Free & open source — no account required
This technical guide provides an in-depth analysis of the json to pydantic engine, best practices for implementation, and data security standards.
Pydantic v2 is not just a validation library — it is the data layer of the Python web ecosystem. FastAPI generates its entire OpenAPI spec from Pydantic models. SQLAlchemy ORM objects can be validated directly into Pydantic via from_attributes=True. Pydantic's TypeAdapter validates top-level JSON arrays without wrapping them in an object. The validation engine was rewritten in Rust (pydantic-core) for v2, delivering 5-50× speedups over v1. Converting your JSON to a Pydantic model means defining a class that simultaneously serves as runtime validator, TypeScript-equivalent type, serializer, and documentation source.
# Input JSON from a weather API
{
"city": "Tokyo",
"country": "JP",
"temperature": 22.5,
"feels_like": 21.0,
"humidity": 65,
"forecast": ["sunny", "partly_cloudy"],
"coords": { "lat": 35.6762, "lon": 139.6503 },
"recorded_at": "2024-01-15T08:30:00Z"
}
# Generated Pydantic v2 Models
from pydantic import BaseModel, Field, field_validator, model_validator
from pydantic import ConfigDict, AliasPath
from typing import Literal, Annotated
from datetime import datetime
class Coordinates(BaseModel):
lat: float = Field(ge=-90.0, le=90.0)
lon: float = Field(ge=-180.0, le=180.0)
class WeatherData(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True,
frozen=True,
)
city: str
country: str = Field(min_length=2, max_length=2, pattern=r'^[A-Z]{2}$')
temperature: float
feels_like: float
humidity: Annotated[int, Field(ge=0, le=100)]
forecast: list[Literal["sunny", "cloudy", "partly_cloudy", "rainy", "snowy"]]
coords: Coordinates
recorded_at: datetime
@field_validator('temperature', 'feels_like')
@classmethod
def validate_temperature(cls, v: float) -> float:
if v < -90 or v > 60:
raise ValueError(f"Temperature {v}°C is outside realistic range [-90, 60]")
return round(v, 1)
@model_validator(mode='after')
def feels_like_near_temperature(self) -> 'WeatherData':
diff = abs(self.temperature - self.feels_like)
if diff > 20:
raise ValueError(f"feels_like ({self.feels_like}) deviates too far from temperature ({self.temperature})")
return self
# Parse from JSON — uses Rust engine (pydantic-core)
data = WeatherData.model_validate_json(json_string)
# Serialize back to JSON
json_out = data.model_dump_json(indent=2)
# Serialize to dict
d = data.model_dump()
# d['recorded_at'] is a datetime object
from pydantic import BaseModel, Field, EmailStr, AnyHttpUrl, SecretStr
from pydantic.networks import IPvAnyAddress
from typing import Annotated
from decimal import Decimal
from uuid import UUID
# Type aliases with constraints — reusable across models
Username = Annotated[str, Field(min_length=3, max_length=50, pattern=r'^[a-z0-9_]+$')]
PositiveInt = Annotated[int, Field(gt=0)]
Percentage = Annotated[float, Field(ge=0.0, le=100.0)]
Money = Annotated[Decimal, Field(decimal_places=2, ge=0)]
class User(BaseModel):
id: UUID
username: Username
email: EmailStr # validates RFC 5322 format
website: AnyHttpUrl | None = None # validates URL format
ip_address: IPvAnyAddress | None = None # IPv4 or IPv6
password: SecretStr # excluded from logs/repr
age: PositiveInt
score: Percentage = 0.0
account_balance: Money = Decimal('0.00')
user = User(
id="123e4567-e89b-12d3-a456-426614174000",
username="py_dev",
email="dev@example.com",
password="secret123",
age=28,
)
print(user.password) # ********** (SecretStr hides value)
print(user.password.get_secret_value()) # secret123
from pydantic import BaseModel, Field, AliasGenerator, ConfigDict
from pydantic.alias_generators import to_camel, to_snake
# Accept camelCase JSON, use snake_case Python
class APIResponse(BaseModel):
model_config = ConfigDict(
alias_generator=to_camel, # snake_case → camelCase in JSON
populate_by_name=True, # also accept snake_case in Python
)
user_id: str
first_name: str
last_name: str
created_at: str
# Parse {"userId": "123", "firstName": "Alice", ...}
r = APIResponse.model_validate({"userId": "123", "firstName": "Alice", "lastName": "Chen", "createdAt": "..."})
print(r.user_id) # "123"
# Serialize: {"userId": "123", "firstName": "Alice", ...}
print(r.model_dump(by_alias=True))
# Per-field alias for one-off key name differences
class StripeEvent(BaseModel):
event_id: str = Field(alias="id")
event_type: str = Field(alias="type")
api_version: str = Field(alias="api_version")
livemode: bool
from pydantic import BaseModel, field_validator, model_validator
from typing import Self
class DateRange(BaseModel):
start_date: str
end_date: str
max_days: int = 90
@field_validator('start_date', 'end_date', mode='before')
@classmethod
def parse_date(cls, v: str) -> str:
# Normalize format before Pydantic validates
from datetime import datetime
try:
return datetime.strptime(v, '%Y-%m-%d').strftime('%Y-%m-%d')
except ValueError:
raise ValueError(f"Date must be YYYY-MM-DD, got: {v}")
@model_validator(mode='after')
def validate_range(self) -> Self:
from datetime import date
start = date.fromisoformat(self.start_date)
end = date.fromisoformat(self.end_date)
if end <= start:
raise ValueError("end_date must be after start_date")
if (end - start).days > self.max_days:
raise ValueError(f"Range cannot exceed {self.max_days} days")
return self
from pydantic import TypeAdapter
from typing import list
class Product(BaseModel):
sku: str
name: str
price: float
# Validate a JSON array of products without a wrapper object
ta = TypeAdapter(list[Product])
products = ta.validate_json('[{"sku":"WGT-001","name":"Widget","price":9.99}]')
# products is list[Product] — fully validated
# Validate a single primitive type
IntTA = TypeAdapter(int)
value = IntTA.validate_json("42") # 42 (int)
# Validate a union type
from typing import Union
MixedTA = TypeAdapter(list[Union[str, int, float]])
values = MixedTA.validate_json('["hello", 42, 3.14]')
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field, EmailStr
from uuid import UUID, uuid4
from datetime import datetime
app = FastAPI(title="My API", version="1.0.0")
class CreateProductRequest(BaseModel):
name: str = Field(min_length=1, max_length=200, examples=["Widget Pro"])
sku: str = Field(pattern=r'^[A-Z]{3}-\d{3}$', examples=["WGT-001"])
price: float = Field(gt=0, examples=[49.99])
category: str = Field(examples=["electronics"])
class ProductResponse(BaseModel):
id: UUID
name: str
sku: str
price: float
category: str
created_at: datetime
model_config = ConfigDict(from_attributes=True) # accept ORM objects
@app.post(
"/products",
response_model=ProductResponse,
status_code=status.HTTP_201_CREATED,
summary="Create a product",
responses={
400: {"description": "Validation error"},
409: {"description": "SKU already exists"},
}
)
async def create_product(request: CreateProductRequest):
# FastAPI: parses body as CreateProductRequest, returns 422 on validation failure
# GET /openapi.json includes full JSON Schema for both request and response
product = await db.products.create(data=request.model_dump())
return product
# Access at /docs — Swagger UI with request/response schemas derived from Pydantic models
from pydantic import BaseModel, ConfigDict
class StrictAPIModel(BaseModel):
model_config = ConfigDict(
strict=True, # no coercion: "42" won't become int 42
frozen=True, # immutable after creation
populate_by_name=True, # accept field name OR alias for input
str_strip_whitespace=True, # auto-strip leading/trailing whitespace
str_min_length=1, # all str fields min length 1 (unless overridden)
extra='forbid', # reject unknown fields (422 on extra JSON keys)
revalidate_instances='always', # re-run validators on model_copy()
)
name: str
email: str
model_validate_json() directly: Don't call json.loads() then model_validate(). The single-step model_validate_json() runs through Rust and is 4-10× faster than Python's JSON parser followed by Pydantic validation.Annotated: Username = Annotated[str, Field(min_length=3)] at the module level lets you reuse the same validated type across models without repeating constraint definitions.extra='forbid' for API request models: Silently accepting unknown fields is a security risk — it hides client bugs and can mask injection attempts. Forbid extra fields on input models.extra='forbid' and strict validation. Response models should use from_attributes=True and may be more permissive. Combining them into one class creates tension between inbound strictness and outbound flexibility.Q: What is the difference between json-to-pydantic and json-to-python-dataclass?
A: Python dataclasses are part of the standard library — no dependencies, no runtime validation, and type hints are checked only by static analysis (mypy/pyright). Pydantic models validate types at runtime, perform coercion, serialize to JSON with full control, and integrate with FastAPI for automatic OpenAPI generation. Use dataclasses for internal data structures; use Pydantic for anything that crosses a trust boundary.
Q: Does Pydantic v2 break v1 code?
A: Yes — v2 has breaking changes. The main ones: parse_obj() → model_validate(), .dict() → .model_dump(), __fields__ → model_fields, validator decorator changes. Pydantic provides a pydantic.v1 compatibility module and a migration guide. Most v1 code migrates in a few hours for small projects.
Q: How do Pydantic models interact with SQLAlchemy?
A: Set model_config = ConfigDict(from_attributes=True) on the Pydantic model. Then call UserResponse.model_validate(orm_user) to read attributes from an ORM object. The Pydantic model acts as a serialization layer — useful for excluding sensitive fields, renaming keys for the API response, and ensuring the response is typed.
Q: Can I generate JSON Schema from Pydantic models?
A: MyModel.model_json_schema() returns a JSON Schema dict that can be used for frontend form validation, API documentation, or cross-language type sharing. FastAPI uses this automatically to build its OpenAPI spec at /openapi.json.
Is the processing local-only?
Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.
Can I use this for enterprise projects?
Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.