Home JSON to Pydantic
JSON to Pydantic model generator
Paste a JSON array and get the Pydantic models that parse it — nested models, Optional fields, and aliases wherever a JSON key is not a legal Python attribute name.
Why it reads every record, not the first one
The usual failure of a JSON-to-type tool is that it looks at one object.
The first record's null becomes the type null; a key that
happens to be absent from it never appears at all; a field that is 7 in the
first row and 7.5 in the fortieth is typed as an integer. Each of those
compiles, passes review and breaks on the second page of results.
This reads every record you paste and merges what it finds:
- A key missing from any record is optional — the marker is on the key, not the value.
- A key that is sometimes null is nullable — a different statement from optional, and both can be true.
- Mixed numbers collapse to one number type. A field holding 7 and 9.5 is a number, not "an integer or a number".
- Recognised string shapes keep their meaning where the target can express it — UUIDs, ISO timestamps, dates, emails and URLs.
- An empty array says nothing about its elements, so it stays unknown rather than being guessed from a sibling record.
So paste more than one record. Paste the awkward ones — the row with the null, the row missing the optional field, the one from the second page. The output is only as good as the range of the sample, and that is a property of the input, not of the tool.
Keys that are not legal Python names
JSON keys can contain spaces, dots and hyphens; Python attributes cannot. A key like
"full name" becomes an attribute named full_name with an alias
back to the original, so parsing the untouched payload still works:
class User(BaseModel):
full_name: str = Field(alias="full name")
Populate by alias when constructing from JSON — model_config =
ConfigDict(populate_by_name=True) in Pydantic v2 if you also want to construct by
the Python name. Without the alias the field would simply never populate, and it would do
so silently.
Imports are emitted only where used
Optional, List, Union, UUID,
date, datetime and Field each appear in the import
block only if the models below actually use them. An unused import is a lint error in most
Python projects, which turns a generated file into something you have to edit before it
passes CI — and a generator whose output needs editing is one people stop using.
Model order and forward references
Nested models are emitted before the models that annotate fields with them. A class
annotation naming a class defined further down the file is a NameError at
import time, so the ordering is correctness rather than preference.
What is deliberately left to you
EmailStris not used. It requires theemail-validatorpackage; the generated file has no dependencies beyond Pydantic itself. Swap it in if you already have that installed.- No validators. Ranges, regexes and cross-field rules are business logic, and a sample cannot see them.
- No enums. A field holding only two values in the sample may hold a third tomorrow.
- Pydantic v2 syntax.
Optional[X]withField(default=None)works on v1 too, butmodel_configand the v2 methods do not exist on v1.
Common questions
Does this target Pydantic v1 or v2?
The generated models use syntax valid in both: Optional[X] with Field(default=None), aliases via Field(alias=...), and standard typing constructs. Configuration you add on top — model_config versus class Config — differs between the versions.
Why is a JSON key with a space turned into an attribute with an underscore?
Because a Python attribute cannot contain a space. The field gets an alias back to the original key so parsing the untouched payload still works — without it the field would silently never populate.
Why is EmailStr not used for email fields?
EmailStr requires the email-validator package. The generated file is meant to run against Pydantic alone; swap str for EmailStr if you already depend on it.
Why are nested models defined first?
A class annotation that names a class defined further down the file raises a NameError when the module is imported. Emission order is correctness, not style.
Are unused imports included?
No. Optional, List, Union, UUID, date, datetime and Field are each imported only if the models use them, so the output does not fail a lint run before you have touched it.
Related
- /json-to-zod — the same job in TypeScript, with runtime validation.
- /json-to-typescript — static types only.
- /json-to-sql-converter — loading the same JSON into a table instead of a model.
- /types — generating JSON that matches a model you already have.
Last updated