Skip to content
Back to Blog
Tutorials

JSON to Python: dataclass, Pydantic & TypedDict (2026 Guide)

Convert JSON to Python classes the right way: dataclass vs Pydantic v2 vs TypedDict, correct Optional typing, camelCase aliases, and pitfalls. Try it free.

12 min read

JSON to Python: dataclass, Pydantic & TypedDict (2026 Guide)

Paste your JSON into the JSON to Python converter and copy the classes it generates. That is the fast path for turning JSON to Python: nothing to install, nothing uploaded, everything runs in your browser. For a one-off model or a quick look at an unfamiliar API response, you are done in seconds.

The harder problem is the one a converter can’t solve for you. It infers correct types, but it can’t make the decision that matters most: which of three outputs you actually want. A dataclass, a Pydantic v2 BaseModel, and a TypedDict can describe the exact same JSON, yet they differ in type-safety strength, runtime behavior, and how you load data into them. Pick wrong and you either carry Pydantic’s dependency for nothing, or you assume a dataclass validates your input when it quietly does not.

This guide covers the decision matrix for all three, how the converter infers types, camelCase handling in each mode, how to parse JSON with the class you generate (including the nested trap that catches people), Pydantic v2 versus v1, and the edge cases worth knowing before you trust the output.

How to convert JSON to Python

Converting JSON to Python takes three steps:

  1. Paste your JSON. Drop an object, array, or raw API response into the input box. Conversion runs instantly and entirely client-side.
  2. Pick dataclass, Pydantic, or TypedDict. Use the Output toggle to switch styles, and rename the default Root to something meaningful like User or ApiResponse.
  3. Copy or download. Grab the generated Python with one click and drop models.py straight into your project.

That covers the common case. If your input is minified or you are not certain it is valid, run it through a JSON formatter first so the converter has clean, well-formed JSON to read. The rest of this guide explains the output so you can fix the cases a tool can’t infer on its own.

How JSON types map to Python

Every JSON value has a Python counterpart, and the mapping is direct:

JSON valuePython type
"text"str
42int (arbitrary precision, no overflow)
3.14, 2e3float
true / falsebool
nullOptional[T] / Optional[Any]
[1, 2, 3]List[T]
{ ... }a named class

Take a typical REST payload:

{ "id": 101, "name": "Ada Lovelace", "email": "ada@example.com", "active": true, "roles": ["admin", "user"] }

In dataclass mode the converter produces this class:

from dataclasses import dataclass
from typing import List


@dataclass
class Root:
    id: int
    name: str
    email: str
    active: bool
    roles: List[str]

One nuance sets Python apart. Its int is arbitrary precision, so a Discord snowflake or any ID past 2^53 stays a plain int with no loss. JavaScript would drop precision on the same value, and Rust would make you choose between i64, u64, and f64. Only a token written with a decimal point or an exponent infers as float. A field that is only ever null can’t be typed from the sample alone, so it falls back to Optional[Any]; paste a representative payload with a filled-in value to get something sharper than Any.

dataclass vs Pydantic v2 vs TypedDict: which should you use?

This is the choice the converter hands back to you. The short version: reach for a dataclass when you want a zero-dependency internal data holder, Pydantic v2 when you parse or validate untrusted input, and TypedDict when you only need static type hints over dicts you already have.

DimensiondataclassPydantic v2TypedDict
DependencyStandard librarypip install pydanticStandard library (typing)
Runtime validationNoYes, validates and coercesNo, hints only
Runtime costLight (real object)Some (validation)Zero (it is a dict)
Nested auto-parsingNo, manualYes, model_validate recursesNo, it is a dict
camelCase aliasesKeeps original keyField(alias=...)Functional syntax
Best forInternal structuresAPI responses, webhooks, configTyping legacy dict code

The three targets look almost identical for a small shape like {"id": 101, "name": "Ada Lovelace", "active": true}. A json to python dataclass gives you a standard-library object:

from dataclasses import dataclass


@dataclass
class Root:
    id: int
    name: str
    active: bool

The json to Pydantic version swaps in BaseModel, which looks similar but earns its keep at runtime:

from pydantic import BaseModel


class Root(BaseModel):
    id: int
    name: str
    active: bool

And a python TypedDict from json describes the shape of a plain dict without allocating anything new:

from typing import TypedDict


class Root(TypedDict):
    id: int
    name: str
    active: bool

Same fields, three very different contracts. The value of generating all three from one sample is that you can toggle between them in the tool and match the mode to the job: Pydantic when you parse data you don’t control, a dataclass when the data is internal, and TypedDict when you just want mypy to understand a dict you already pass around.

How the converter infers classes

Three rules cover nearly everything you feed it: one class per object shape, key-by-key merging for arrays, and careful handling of missing values.

Structural inference: one named class per object

Each distinct object shape becomes its own named class. Nested objects are not inlined; they get hoisted into separate, referenced definitions:

{ "repo": "pydantic", "owner": { "login": "samuelcolvin", "id": 100 } }
from dataclasses import dataclass


@dataclass
class Owner:
    login: str
    id: int


@dataclass
class Root:
    repo: str
    owner: Owner

The nested owner becomes its own Owner class, referenced by field. Notice the order: the child class is emitted before the parent that uses it, so the module runs without from __future__ import annotations. Identical shapes are deduplicated too, so two fields with the same structure share one class instead of producing copies.

Array merging and Optional fields

When you pass an array of objects, the converter merges them key by key into a single element type. A key that is present in some elements but missing from others becomes Optional:

{ "users": [{ "id": 1, "nick": "x" }, { "id": 2 }] }
from typing import List, Optional, TypedDict


class User(TypedDict):
    id: int
    nick: Optional[str]


class Root(TypedDict):
    users: List[User]

id appears in every element, so it stays required. nick is absent from the second user, so it becomes Optional[str]. This is exactly why a single object is a weak sample: the tool can only mark a field Optional when it actually sees the key missing somewhere. Feed a representative array and the inference reflects the real shape.

Optional, None, and empty arrays

A few values carry no type on their own, and the converter is honest about it. A key missing from some array items becomes Optional[T]. A field that is only ever null becomes Optional[Any], because JSON null alone tells you nothing about the intended type. An empty array or one with mixed element types falls back to List[Any]. None of these are bugs; they are the tool declining to guess. Replace each Any with a concrete type once you have a sample that shows one, and remember that Python’s arbitrary-precision int means a large ID never forces you into a wider numeric type the way it would in Rust or JavaScript.

Handling camelCase keys in each mode

JSON keys are often camelCase while idiomatic Python fields are snake_case. There is no single right answer here, so each mode resolves it differently, and the difference is worth understanding before you copy the output.

dataclass. A dataclass has no built-in alias mechanism. To keep Root(**data) working, the converter keeps the original key as the field name whenever it is a valid Python identifier, so publicRepos stays publicRepos. Forcing it to snake_case would break **data with a TypeError.

Pydantic v2. Here you get the idiomatic result. Fields are renamed to snake_case with a Field(alias=...) mapping back to the exact JSON key, plus model_config = ConfigDict(populate_by_name=True) so you can build the model by field name or by alias:

from pydantic import BaseModel, ConfigDict, Field


class Root(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    login: str
    public_repos: int = Field(alias="publicRepos")
    created_at: str = Field(alias="createdAt")

That is what lets Root.model_validate(api_json) round-trip real camelCase payloads while your code reads naturally.

TypedDict. Plain identifier keys are used directly. As soon as a non-identifier key appears, the converter switches to the functional TypedDict('User', {...}) syntax to preserve the exact key, which the next section covers.

Python keywords and non-identifier keys

JSON keys are just strings, so nothing stops an API from sending class, from, or first-name. Each would be a syntax error as a bare Python attribute, so the converter sanitizes them.

A key that is a Python keyword gets a trailing underscore: class becomes class_, from becomes from_. In Pydantic mode it also keeps a Field(alias=...) so the model still reads the original wire key:

from pydantic import BaseModel, ConfigDict, Field


class Root(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    class_: str = Field(alias="class")
    from_: str = Field(alias="from")

Keys with hyphens, spaces, or a leading digit are sanitized to valid identifiers in dataclass and Pydantic modes. TypedDict is the one mode that can keep the literal key, using the functional form so "first-name" survives untouched:

from typing import TypedDict

Root = TypedDict("Root", {"first-name": str, "2fa": bool})

The result always runs, and in TypedDict mode the key stays byte-for-byte what the API sent.

Parsing JSON with the generated class

Generating the class is half the job. Loading JSON into it is where the three modes diverge sharply, and where one popular assumption goes wrong.

dataclass. For a flat object, json.loads plus Root(**data) works directly:

import json
from dataclasses import dataclass
from typing import List


@dataclass
class Root:
    id: int
    name: str
    roles: List[str]


data = json.loads('{"id": 101, "name": "Ada Lovelace", "roles": ["admin"]}')
root = Root(**data)
print(root.name, root.roles)

But a dataclass does not recurse. If the JSON has nested objects, Root(**data) fills the top level and leaves every child as a plain dict, not the Owner class you generated. To parse a whole tree, build the children yourself, use a library like dacite or pydantic.dataclasses, or switch the tool to Pydantic mode. This is the pitfall most converters never mention.

Pydantic v2. One call parses and validates the entire tree:

import json
from pydantic import BaseModel


class Root(BaseModel):
    id: int
    name: str


root = Root.model_validate(json.loads('{"id": 101, "name": "Ada Lovelace"}'))
print(root.name)

A type mismatch raises a clear ValidationError instead of passing silently. That is the payoff for the dependency: the model is a runtime validator, so a model_validate call that returns is a guarantee the data matched, not just a type checker’s belief that it did.

TypedDict. At runtime a TypedDict is an ordinary dict. Annotating data: Root = json.loads(text) is purely for the static checker; nothing is validated when the code runs. If you need real enforcement, run mypy in CI or move to Pydantic. Choosing a mode comes down to how much runtime guarantee you want, and when a contract needs to go beyond types, the JSON Schema validation guide covers enforcing structure end to end.

Pydantic v2 vs v1: what changed

The converter emits v2 syntax. Plenty of codebases still run v1, and pasting v2 output into a v1 project fails on renamed methods rather than logic. Here is the mapping:

TaskPydantic v2 (tool output)Pydantic v1
Parse a dictmodel_validate(data)parse_obj(data)
Parse raw JSONmodel_validate_json(text)parse_raw(text)
Configure the modelmodel_config = ConfigDict(...)inner class Config
Serialize to dictmodel_dump()dict()
Serialize to JSONmodel_dump_json()json()

The alias behavior also tightened: in v2 you need populate_by_name=True alongside Field(alias=...) to construct a model by either the field name or the alias, which is why the tool includes it. The practical rule is short. Upgrade to v2 if you can, since it is faster and the target of current development; otherwise rewrite model_validate back to parse_obj and ConfigDict back to an inner Config.

json-to-python vs quicktype vs datamodel-code-generator vs manual

There is no single best way to generate a Python class from JSON. It depends on where the JSON lives and what you have to start from.

ApproachBest forNote
Online converter (this tool, jsonlint, codeshack)One-off conversions, sensitive payloads, zero installInfers from a sample, fully client-side
quicktypeMulti-language output, pipeline codegenAlso sample-driven; Python support is thinner on aliases and mode choice
datamodel-code-generatorGenerating Pydantic from a JSON Schema or OpenAPIInput is a schema, not a sample, so it is more authoritative when you have one
Writing classes by handTiny payloads, learning the syntaxFull control, but tedious and easy to drift

The difference to keep in mind: an online converter and quicktype both infer types from a sample of JSON, while datamodel-code-generator reads a JSON Schema or OpenAPI spec as its source of truth. Use the sample tools for exploration and one-offs; reach for the schema-driven one when a contract already exists. If your codebase is TypeScript instead of Python, the same sample-driven approach applies with the JSON to TypeScript converter, and the JSON to TypeScript interface guide covers that side in depth. For the runtime-validation angle in a strongly typed compiled language, the JSON to Rust struct guide makes a useful contrast: serde is Rust’s Pydantic.

Common pitfalls when generating Python from JSON

Generated classes are a starting point. Watch for these before you rely on the output with live data.

  • One sample rarely reveals every shape. Optional can only be inferred from array elements that actually differ. Paste a representative array so the optionality is accurate, not a guess from one lucky object.
  • A dataclass does not validate. Root(**data) only assigns fields; it checks no types and recurses into nothing. If you need enforcement, use Pydantic.
  • Nested JSON breaks Root(**data). Only the top level is populated; children stay dicts. Switch to Pydantic’s model_validate, or use dacite.
  • An always-null field becomes Optional[Any]. Treat it as a placeholder, not a normal optional, and give it a real type once you know one.
  • v2 output in a v1 project. model_validate will be undefined. Upgrade to v2 or rewrite it as parse_obj.
  • A date left as str won’t do date math. Switch the field to datetime, or in Pydantic use a datetime field so it parses ISO 8601 for you.

Frequently asked questions

Should I use a dataclass, Pydantic, or TypedDict for JSON?

Use a dataclass for a zero-dependency internal data holder. Use Pydantic v2 when you parse or validate untrusted input, because it enforces types at runtime. Use TypedDict to add static hints to a dict you already have, with no runtime cost. When the data comes from outside your control, default to Pydantic.

Does Pydantic validate JSON at runtime?

Yes. Root.model_validate(data) validates and coerces types as the code runs, raising a ValidationError on bad input. A dataclass does neither; Root(**data) only assigns fields. A TypedDict does neither either; it is a hint for static checkers like mypy and behaves as a plain dict at runtime.

How do I type a field that JSON sometimes omits?

Type it as Optional[T]. When a key appears in only some array elements, the converter marks it Optional automatically. In Pydantic, give the field a default such as = None so a missing key does not trigger a validation error. A dataclass field can default the same way with field(default=None).

Why did my dataclass fail to parse nested JSON?

Because Root(**data) does not recurse. It fills the top-level fields and leaves nested objects as plain dicts rather than the child classes you generated. To parse the whole tree in one call, switch to Pydantic’s model_validate, or add recursion with dacite or pydantic.dataclasses.

What Python type does a large JSON integer become?

int. Python integers are arbitrary precision, so a snowflake or Discord ID past 2^53 stays a plain int with no loss. You never hit the precision drop JavaScript has, and you never have to choose between i64 and u64 the way Rust forces you to.

How are camelCase JSON keys handled in each mode?

A dataclass keeps the original key so Root(**data) still works. Pydantic renames fields to snake_case with Field(alias="camelKey") plus populate_by_name=True, so it round-trips real API data. TypedDict uses the functional TypedDict('Name', {...}) syntax to preserve any key that is not a valid identifier.

How do I handle a JSON key that is a Python keyword like class?

The converter appends a trailing underscore, turning class into class_ so the code is valid. In Pydantic mode it also adds Field(alias="class") mapping the field back to the original wire key, so the name is legal and the model still reads the real payload.

Is my JSON private when using an online JSON to Python converter?

Yes. Conversion runs 100% in your browser with JavaScript. Your JSON (including tokens, IDs, and customer data) never leaves the page and is never sent to a server. It even works offline once the page has loaded. When you are ready, try the JSON to Python converter on your own payload.

Tags: python json pydantic type-safety developer-tools

Related Articles

View all articles