JSON to Python / Pydantic
Generate Pydantic v2 models, dataclasses or TypedDicts from sample JSON — entirely on this device.
Press ? for shortcuts
Do it offline:$ npx quicktype sample.json --lang python --pydantic-base-model --just-types -o models.py
How to use it
- Paste a JSON payload or drop a .json file into the input pane — the Python regenerates a moment after you stop typing.
- Add a tab for every other shape the endpoint returns; a key missing from one comes back with a None default.
- Pick Pydantic v2, dataclass or TypedDict, choose the target version, and name the top-level model.
- Copy the module or download it as a .py file — the alias control decides where the wire key lands.
Paste one or more JSON samples and get Python ready to drop into a module: Pydantic v2 models by default, or standard-library dataclasses, or TypedDicts. Field names become snake_case and the original wire key rides along in a Field(alias=…) or a trailing comment. Target 3.7, 3.9 or 3.10. The whole render happens in this tab, offline.
Examples
{"order_id": 7, "userId": 1, "items": [{"sku": "a-1", "qty": 2}]}from pydantic import BaseModel, ConfigDict, Field
class Item(BaseModel):
sku: str
qty: int
class Order(BaseModel):
model_config = ConfigDict(populate_by_name=True)
order_id: int
user_id: int = Field(alias="userId")
items: list[Item]
Two object shapes in, two models out. order_id already reads as Python and stays bare; userId becomes user_id with the camelCase spelling pinned in an alias. populate_by_name appears only in classes that own one, and accepts either name.
{"first-name": "Ada", "zipCode": "94110", "tags": ["a"]}from dataclasses import dataclass
from typing import List
@dataclass
class Root:
first_name: str # JSON key: "first-name"
zip_code: str # JSON key: "zipCode"
tags: List[str]
A dataclass has no alias mechanism, so the wire key rides in a trailing comment documenting the mapping your deserializer must still perform. Targeting 3.7 switches the annotation to List[str] from typing — builtin list became subscriptable in 3.9.
{"id": 1, "label": "x"}
{"id": 2}from typing import TypedDict
from typing_extensions import NotRequired
class Root(TypedDict):
id: int
label: NotRequired[str | None]
label is absent from the second sample, so the TypedDict marks the key as NotRequired — something no model can express, because a model describes values, not presence. NotRequired reached typing only in 3.11, hence the typing_extensions import.
{"a": null, "b": 1}from pydantic import BaseModel
class Root(BaseModel):
b: int
a: None = None
Edge case: a is null everywhere, so no type can be inferred and it renders as None; one more sample with a real value fixes it. It also moved behind b, because a field without a default may not follow one that has one.
{"class": "economy", "2fa": true}from pydantic import BaseModel, ConfigDict, Field
class Root(BaseModel):
model_config = ConfigDict(populate_by_name=True)
root_class: str = Field(alias="class")
the_2_fa: bool
class is a keyword and 2fa starts with a digit, so both get rewritten. The keyword resolves to one sample key and keeps its alias; the digit-leading one resolves to nothing, so no alias is invented and a note admits the loss.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
Optional[T] and NotRequired[T] answer different questions
Optional[T] says the value may be None; NotRequired[T] says the key may not be there at all. Models and dataclasses express only the first, so a key missing from one sample arrives None-defaulted. Reach for TypedDict when absence is the point.
A default of None is not a nullable column
In a dataclass, a None default only means the constructor may be called without that argument. Pydantic v2 reads optionality from the annotation, never from the default: str | None is the part that admits None, and the default only makes the field non-required.
The three targets differ in two places
3.10 writes list[int] and str | None. 3.9 keeps list[int] but falls back to Optional[str]. 3.7 writes List[int] and Optional[str], importing both. Nothing else moves, so switching targets late is cheap; an unsupported version is replaced by the default with a note.
TypedDict output is our transform
quicktype has no TypedDict target. This tool takes the dataclass render, drops the decorator, rebases the class and rewrites None-defaulted fields as NotRequired — defaults cannot exist in a TypedDict body. Every typing_extensions import is called out.
What the top level has to be
The input must be a JSON object or an array of them. A bare string or number declares nothing and is refused, never answered with an empty file. An array generates from its element type, named RootElement; an empty object yields a class whose body is pass.
Repeated shapes share one class
Two fields holding identically shaped objects get one class between them, and a note names the sharing. Nesting deeper than 256 levels is refused with the cap in the message — one class is emitted per level, and nothing is truncated to fit.
Common errors
Unexpected '}' at line 1, column 16 — probable cause: trailing comma after the last property.- Generation starts only once every tab parses as strict JSON. The diagnostic carries line, column and a caret frame, and names the failing tab when more than one is open.
Python code generation produced no classes — the top-level value must be a JSON object or an array of objects.- A top-level scalar leaves nothing to model, and an empty file would look like success. Wrap the value in an object.
Input is larger than the 10 MB limit for this tool.- Past the 10 MB cap the whole paste is declined; nothing is silently cut. Point the quicktype command printed below at the complete file instead.
Samples total 2.0 MB — above the 2 MB advisory band. Generate from a representative sample, not the whole export.- Model shapes come from structure, not volume — a handful of varied records yields the same classes as a giant export. Past the advisory band nothing runs until you ask.
FAQ
Is this Pydantic v2 or v1?
v2 only. Models carry model_config = ConfigDict(populate_by_name=True) rather than the v1 inner Config class. Nothing is back-ported for v1.
Why are there no validators or field constraints?
Sample data cannot imply them. A string of length 12 is no evidence of a max_length rule, so gt, pattern and friends are left out — add what your API really guarantees. JSON Schema input, attrs and ORM models are out of scope too.
Does my JSON leave the browser while models generate?
No. The generator is JavaScript running inside this page and samples are never posted anywhere — open the network panel while generating to confirm.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Render now, even past the 2 MB size where runs turn explicit.
- ⌘/Ctrl + S
- Download the generated module as a .py file.
- ⌘/Ctrl + C
- Copy the whole module while nothing is selected; a text selection copies normally.
- ⌘/Ctrl + Shift + C
- Copy the whole module even while a selection exists.
- t
- Switch theme; ignored while a field has focus.
- ? or ⌘/Ctrl + /
- Show these bindings in the help dialog.
- Esc
- Close the topmost dialog; panes keep what they hold.