pythonpydanticdataclassjson

JSON to Python — dataclass, TypedDict, and Pydantic v2

·8 min read

Why Generate Python Classes from JSON?

Working with JSON in Python as plain dicts (data["user"]["name"]) is error-prone and provides no IDE autocompletion. Generating a typed class lets you access data as obj.user.name with autocomplete, type checking, and runtime validation.

There are three main approaches: @dataclass, TypedDict, and Pydantic BaseModel. Each serves a different use case.

JSON Type Mapping to Python

JSON typePython typeNotes
stringstr
integerint
floatfloat
booleanboolJSON true/false → Python True/False
nullNone / Optional[T]
objectClass / TypedDict / dictNested → nested class
arraylist[T]Element type inferred

Approach 1: @dataclass — Zero Dependencies

Dataclasses give you real class instances, attribute access syntax, and automatic __init__ with no third-party dependencies:

python
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional

@dataclass
class Address:
    city: str
    state: str
    pincode: str

@dataclass
class User:
    id: int
    name: str
    email: str
    active: bool
    score: float
    address: Address
    tags: list[str] = field(default_factory=list)
    middle_name: Optional[str] = None

# Instantiate directly
user = User(
    id=1, name="Ravi", email="ravi@example.com",
    active=True, score=98.5,
    address=Address(city="Surat", state="Gujarat", pincode="395001"),
    tags=["admin", "developer"]
)
print(user.address.city)    # => "Surat"
print(user.tags[0])         # => "admin"

Dataclasses do not automatically parse from dicts. Use dacite for dict-to-class conversion:

python
# pip install dacite
from dacite import from_dict, Config
import json

raw = json.loads(json_string)
user = from_dict(
    data_class=User,
    data=raw,
    config=Config(strict=True),  # fail on unknown keys
)
print(user.address.pincode)  # => "395001"

Approach 2: TypedDict — Type Hints on Plain Dicts

TypedDict is just a type hint — at runtime it is a completely ordinary Python dict with zero overhead. Use it when you want IDE type checking without creating class instances:

python
from typing import TypedDict, Optional

class Address(TypedDict):
    city: str
    state: str
    pincode: str

class User(TypedDict, total=False):
    id: int
    name: str
    email: str
    active: bool
    score: float
    address: Address
    tags: list[str]
    middle_name: Optional[str]

Usage — the value is still a plain dict at runtime:

python
import json
from typing import cast

raw = json.loads(json_string)
user = cast(User, raw)    # type-only cast — no runtime effect

print(user["name"])         # still dict access syntax
print(user["address"]["city"])

# Mypy/pyright will catch typos like user["nmae"]

Use TypedDict for function signatures, return type annotations, and anywhere you want type hints on existing dict data without converting it to class instances.

Approach 3: Pydantic v2 — Runtime Validation + Serialization

Pydantic is the most powerful option. It validates data at parse time, provides detailed error messages on failure, serializes back to JSON, and generates JSON Schema. Used by FastAPI, SQLModel, and thousands of production services:

python
# pip install pydantic[email]
from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Optional
import json

class Address(BaseModel):
    city: str
    state: str
    pincode: str = Field(pattern=r"^\d{6}$")  # 6-digit pincode

    model_config = {"extra": "forbid"}  # reject unknown fields

class User(BaseModel):
    id: int = Field(gt=0)
    name: str = Field(min_length=1, max_length=100)
    email: EmailStr                     # validates email format
    active: bool = True                 # default value
    score: float = Field(ge=0, le=100)
    address: Address
    tags: list[str] = []
    middle_name: Optional[str] = None

    model_config = {"extra": "forbid"}

    @field_validator("name")
    @classmethod
    def name_must_not_contain_numbers(cls, v: str) -> str:
        if any(c.isdigit() for c in v):
            raise ValueError("Name cannot contain numbers")
        return v.strip()

# Parse and validate from dict
raw = json.loads(json_string)
user = User.model_validate(raw)         # raises ValidationError if invalid
print(user.name)                        # => "Ravi"
print(user.address.city)               # => "Surat"
print(type(user.score))                # => <class 'float'>

# Serialize back to JSON
print(user.model_dump_json(indent=2))
print(user.model_dump())               # => dict

# Generate JSON Schema
schema = User.model_json_schema()
print(json.dumps(schema, indent=2))    # ready for OpenAPI

# Safe parse (no exception)
result = User.model_validate(raw, strict=False)

When validation fails, Pydantic raises ValidationError with detailed per-field messages:

python
from pydantic import ValidationError

try:
    bad = User.model_validate({"id": -1, "email": "not-an-email"})
except ValidationError as e:
    for error in e.errors():
        print(f"Field: {error['loc']}, Error: {error['msg']}")
# Field: ('id',), Error: Input should be greater than 0
# Field: ('email',), Error: value is not a valid email address
# Field: ('name',), Error: Field required

Integration with FastAPI

Pydantic is FastAPI's native data validation layer — request bodies and response models are Pydantic models:

python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr

app = FastAPI()

class CreateUserRequest(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    email: EmailStr
    role: str = "viewer"

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    role: str

@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(req: CreateUserRequest) -> UserResponse:
    # FastAPI validates the JSON body automatically using Pydantic
    # Returns 422 Unprocessable Entity if validation fails
    new_user = db.create_user(req.name, req.email, req.role)
    return UserResponse.model_validate(new_user)

Which Approach Should You Use?

SituationBest choiceWhy
FastAPI request/response modelsPydanticNative integration, auto-validation
External API data ingestionPydanticCatches bad data at the boundary
Internal data structures@dataclassClean, typed, zero-dep
Type hints on existing dictsTypedDictZero runtime overhead
ORM result setsTypedDict or dataclassData is already validated by DB
Config files / env varsPydanticField validators + error messages

Use JSONKit's JSON to Python tool to generate all three patterns — @dataclass, TypedDict, and Pydantic BaseModel — from any JSON object in seconds. Paste your JSON, choose the output style, copy the code.

Try JSON to Python

Generate @dataclass, TypedDict, or Pydantic v2 BaseModel from any JSON object.