Output Model Types¶
datamodel-code-generator supports multiple output model types. This page compares them to help you choose the right one for your project.
Quick Comparison¶
| Model Type | Validation | Serialization | Performance | Use Case |
|---|---|---|---|---|
| Pydantic v2 | Runtime | Built-in | Fast | New projects, APIs, data validation |
| Pydantic v2 dataclass | Runtime | Built-in | Fast | Pydantic validation with dataclass syntax |
| dataclasses | None | Manual | Fastest | Simple data containers, no validation needed |
| TypedDict | Static only | Dict-compatible | N/A | Type hints for dicts, JSON APIs |
| msgspec | Runtime | Built-in | Fastest | High-performance serialization |
Pydantic v2 (Recommended)¶
Use --output-model-type pydantic_v2.BaseModel
Pydantic v2 is recommended for new projects. It offers better performance and a modern API.
from pydantic import BaseModel, Field, RootModel
class Pet(BaseModel):
id: int = Field(..., ge=0)
name: str = Field(..., max_length=256)
tag: str | None = None
class Pets(RootModel[list[Pet]]):
root: list[Pet]
When to use¶
- New projects requiring data validation
- APIs requiring data validation
- Projects needing JSON Schema generation from models
Pydantic v2 dataclass¶
Use --output-model-type pydantic_v2.dataclass
Pydantic v2 dataclass combines the familiar dataclass syntax with Pydantic's validation capabilities.
from pydantic.dataclasses import dataclass
from typing import Optional
@dataclass
class Pet:
id: int
name: str
tag: Optional[str] = None
When to use¶
- Want to use dataclass syntax with Pydantic validation
- Migrating from dataclasses but need validation
- Prefer decorator-based class definition
Targeting a Pydantic version¶
Pydantic output depends only on the inputs and options, never on the Pydantic version installed next to
datamodel-codegen. Use --target-pydantic-version to choose the oldest Pydantic release the generated code must
run on. Pass --target-pydantic-version 2 when the generated code must also run on older Pydantic 2 releases.
When the target is unset:
- Feature forms use the newest supported forms, the same as
2.12. - Config key naming is unchanged:
--allow-population-by-field-nameemitspopulate_by_name=True, as with2. Only an explicit2.11,2.12or2.14emitsvalidate_by_name=True. --use-missing-sentinelkeeps the experimentalpydantic.experimental.missing_sentinelimport, as with2.12.
| Generated form | 2 (Pydantic 2.0+) |
Unset, 2.11, 2.12, 2.14 |
|---|---|---|
Constrained strings with --use-annotated (2.1+) |
Annotated[str, Field(...)] |
Annotated[str, StringConstraints(...)] |
pydantic_v2.dataclass aliases that are not identifiers (2.4+) |
Field(validation_alias=..., serialization_alias=...) assignment |
Field(alias=...) |
| Deprecated fields (2.7+) | Field(json_schema_extra={'deprecated': True}) |
Field(deprecated=True) |
| Models used as dictionary keys (2.8+) | Defined before the models that use them as keys | Schema order |
--alias-generator field aliases (2.8+) |
alias= on every BaseModel field |
alias= only where the generated alias differs |
| Type alias patterns that need Python's regex engine (2.8+) | String pattern; every model that reaches the alias, directly or through other models, sets regex_engine="python-re" |
Compiled re.compile(...) pattern |
pydantic_v2.dataclass aliases of other models (2.10+) |
Alias: TypeAlias = Model |
Alias = TypeAliasType("Alias", Model) |
Explicit aliases naming a BaseModel attribute (2.10+) |
Rejected when the attribute starts with model_ |
Rejected only when it starts with model_validate or model_dump |
--use-annotated fields whose type is defined later in the module (2.11+) |
name: Type = Field(...) assignment |
name: Annotated[Type, Field(...)] |
| Config key naming | Unset, 2 |
2.11, 2.12, 2.14 |
|---|---|---|
--allow-population-by-field-name |
populate_by_name=True |
validate_by_name=True |
2.12 also enables options that need Pydantic 2.12, such as --use-missing-sentinel.
--use-missing-sentinel import |
Unset, 2.12 |
2.14 |
|---|---|---|
MISSING (2.14 deprecates the experimental path) |
from pydantic.experimental.missing_sentinel import MISSING |
from pydantic import MISSING |
bytes patterns the Rust regex engine rejects |
Unset, 2, 2.11, 2.12 |
2.14 |
|---|---|---|
Model regex engine (2.14 compiles the patterns of plain and optional bytes fields, 2.11 to 2.13 ignore them) |
Default engine | regex_engine="python-re" |
The bytes rule is not applied to an unset target, because python-re changes how the other patterns of the model
match on every Pydantic release. A bytes pattern with syntax such as a backreference then fails at import on
Pydantic 2.14 unless --target-pydantic-version 2.14 is passed, and on Pydantic 2.12 for a bytes | MISSING field
(--use-missing-sentinel), whose pattern 2.12 compiles as well. Pydantic 2.0 to 2.10 rejects any rendered bytes
pattern at import. See Regular Expression Patterns.
pydantic_v2.dataclass required fields written as = Field(...) |
Unset, 2, 2.11, 2.12 |
2.14 |
|---|---|---|
Position when the schema lists them after a field with a default of the same class (2.13+ rejects that order in inspect.signature) |
Schema order | Before the fields with defaults |
A required pydantic_v2.dataclass field that needs Field(...), for an alias, a description or a constraint, is
written as an assignment, so it can follow a field with a default:
@dataclass
class Pet:
plain: str
nick: str | None = "x"
first_name: str = Field(..., alias="first-name")
With the target unset or older than 2.14, such a class keeps the order of datamodel-code-generator 0.83.0. It
validates and constructs on Pydantic 2.4 and later, the first release that accepts the non-identifier alias, but on
Pydantic 2.13 and later its signature cannot be read: inspect.signature(Pet) raises
ValueError: non-default argument follows default argument, which affects tools that read the signature. On Python
3.10 with Pydantic 2.13 and later, a dataclass that inherits from such a class fails at import with the same error,
because Python 3.10 reads the signature while it creates the subclass. Pass --target-pydantic-version 2.14 in either
case: first_name is then written before nick. The generator warns about each class that keeps such an order, in its
own fields or through its bases.
The order is not changed for an unset or older target, because positional arguments follow the field order:
Pet("p", "a", "b") sets nick="a" in schema order and first_name="a" with 2.14. Keyword arguments work with both
orders.
With --use-annotated a required field keeps its metadata in Annotated and comes before the fields with defaults on
every target. A non-identifier alias of such a field is written as an assignment with 2 (see the 2.4 row above); the
field then stays with the required fields, after those without an assignment. A required field that a subclass adds
after an inherited field with a default is written with kw_only=True for every target.
Patterns that need Python's regex engine, such as lookaround assertions (see
Regular Expression Patterns), rely on regex_engine="python-re", which
Pydantic added in 2.5. With every target, a model that has such a pattern fails at import on Pydantic 2.0 to 2.4. A type
alias (--use-type-alias, and the root schemas of pydantic_v2.dataclass output) has no config of its own:
- With
2its pattern stays a string, and every generatedBaseModelorpydantic_v2.dataclassthat reaches the alias, directly or through other models and their base classes, setsregex_engine="python-re", so the generated models validate on Pydantic 2.5 and later. ARootModelthat reaches the alias is written asRootModel[Any]and keeps the alias in itsrootannotation, because Pydantic builds the generic before the config applies. - With
2, nothing selects the engine outside the generated models. A hand-written model that embeds a generated model or alias must setregex_engine="python-re"itself, and an alias validated on its own needsTypeAdapter(Alias, config=ConfigDict(regex_engine="python-re")). On Pydantic 2.8 to 2.10 this is a difference from an unset target, because no target lies between2and2.11. - Unset, or with
2.11and later, the alias carries a compiledre.compile(...)pattern, which needs Pydantic 2.8 and validates on its own.
The combined pattern constraints of XML Schema input stay compiled for every target, 2 included, and need Pydantic
2.8; see XML Schema limitations.
With 2 and --use-annotated, a BaseModel, RootModel or pydantic_v2.dataclass field whose type names a model
defined later in the same module is written as an assignment, as it is without --use-annotated. Pydantic before 2.11
cannot evaluate such an annotation while it creates the class, and then mishandles the Field() inside
Annotated[...]: depending on the release it drops the alias and the description, ignores validate_default, or fails
on a constraint. Required dataclass fields written this way move after the other required fields. Fields that reference
only their own model or earlier models, and output without deferred annotations (--disable-future-imports), keep the
Annotated form.
For a field written this way, type checkers that read Field() assignments, such as pyright, take the alias as the
constructor argument name and treat a positional default such as Field(None, alias=...) as a required argument. Add
--use-default-kwarg to emit Field(default=None, ...) instead.
With 2, --alias-generator output names every alias in Field(alias=...). Type checkers that read the alias as the
constructor argument name, such as pyright, then expect the alias instead of the field name; at runtime
populate_by_name=True still accepts both.
dataclasses¶
Use --output-model-type dataclasses.dataclass
Python's built-in dataclasses for simple data containers without runtime validation.
from dataclasses import dataclass
from typing import Optional
@dataclass
class Pet:
id: int
name: str
tag: Optional[str] = None
Options for dataclasses¶
| Option | Description |
|---|---|
--frozen-dataclasses |
Generate immutable dataclasses (frozen=True) |
--keyword-only |
Require keyword arguments (kw_only=True, Python 3.10+) |
--dataclass-arguments |
Custom decorator arguments as JSON |
# Frozen, keyword-only dataclasses
datamodel-codegen --input schema.json --output-model-type dataclasses.dataclass \
--frozen-dataclasses --keyword-only --target-python-version 3.10
When to use¶
- Simple data structures without validation needs
- Performance-critical code where validation overhead matters
- Interoperability with code expecting dataclasses
TypedDict¶
Use --output-model-type typing.TypedDict
TypedDict provides static type checking for dictionary structures.
from typing import TypedDict, NotRequired
class Pet(TypedDict):
id: int
name: str
tag: NotRequired[str]
When to use¶
- Working with JSON APIs where data remains as dicts
- Static type checking without runtime overhead
- Gradual typing of existing dict-based code
Boundary payloads and partial updates¶
TypedDict is a good fit for data at an HTTP boundary. A PATCH payload can distinguish three cases:
- the key is missing, so the existing value should stay unchanged
- the key is present with
null, so the value should be cleared - the key is present with a value, so the value should be replaced
Use --strict-nullable when the difference between a missing key and a
nullable value matters:
datamodel-codegen --input user-patch.schema.json \
--output-model-type typing.TypedDict \
--strict-nullable \
--target-python-version 3.13 \
--output model.py
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "UserPatch",
"type": "object",
"required": ["id"],
"properties": {
"id": {"type": "string", "readOnly": true},
"nickname": {"type": ["string", "null"]},
"bio": {"type": "string"}
},
"additionalProperties": false
}
from typing import NotRequired
from typing_extensions import TypedDict
class UserPatch(TypedDict, closed=True):
id: str
nickname: NotRequired[str | None]
bio: NotRequired[str]
Here nickname: NotRequired[str | None] means the key may be omitted, and
None is still a real value when the key is present. This follows
PEP 655, which added Required and
NotRequired for per-key presence in TypedDict.
If the schema marks response-only fields with readOnly, combine this with
--use-frozen-field to generate ReadOnly for TypedDict output:
datamodel-codegen --input user-patch.schema.json \
--output-model-type typing.TypedDict \
--strict-nullable \
--use-frozen-field \
--target-python-version 3.13 \
--output model.py
from typing import NotRequired, ReadOnly
from typing_extensions import TypedDict
class UserPatch(TypedDict, closed=True):
id: ReadOnly[str]
nickname: NotRequired[str | None]
bio: NotRequired[str]
ReadOnly comes from PEP 705. The
closed=True output comes from PEP 728
and represents additionalProperties: false for type checkers that understand
closed TypedDicts.
Background
The distinction between missing keys, None, and unset update arguments is
covered in Koudai Aono's PyCon US 2026 talk
Beyond Optional in Real-World Projects.
In datamodel-code-generator today, TypedDict and msgspec preserve that
boundary shape most directly. Pydantic models and dataclasses are usually a
better fit after the boundary has been normalized into application data.
msgspec¶
Use --output-model-type msgspec.Struct
msgspec offers high-performance serialization with validation.
pip install msgspec
datamodel-codegen --input schema.json --output-model-type msgspec.Struct --output model.py
from msgspec import Struct, UNSET, UnsetType
class Pet(Struct):
id: int
name: str
tag: str | UnsetType = UNSET
Discriminated unions¶
msgspec only decodes a union of structs when every struct is tagged, and a tagged struct cannot also have a field
named like its tag_field. By default the output keeps the 0.83.0 form, in which a variant whose discriminator
value comes from the discriminator mapping, an enum or the schema name keeps the discriminator as a field:
Cat(petType='cat'), cat.petType and decoding Cat on its own work, but decoding the union Cat | Dog raises
msgspec's TypeError. That form only tags a variant whose discriminator property is a single literal such as a
const, or one that a second discriminated union uses, and its tag_field is the generated field name, which
differs from the property name when the field is renamed.
--use-msgspec-tagged-unions writes a union as
tagged structs as a whole instead, with the property name of the document as tag_field:
The union then decodes, while petType is no longer a constructor argument or an attribute, and Cat decoded on
its own accepts a payload without petType. When a variant cannot take exactly one tag for the union, or a base
or derived struct of a variant keeps the property as a field, the union tags none of its variants, the ones with
a const included, and a warning names the reason. A variant that another union tags keeps that tag.
Enums that list null¶
msgspec only decodes an Enum whose members are all str or all int values. By default the output keeps the
0.83.0 form of an enum that lists null without type: string or nullable: true, such as
type: [integer, "null"] with enum: [1, 2, null], which is an Enum with a None member:
Size.int_1 works, but decoding a struct with a Size field raises msgspec's TypeError.
--use-msgspec-nullable-enum-alias writes
it as a type: string or nullable enum is written instead, an Enum without the None member and an alias that
adds None:
msgspec then decodes the members and null, while code that used Size.int_1 or Size.NoneType_None uses
SizeEnum.int_1 and None. The default of a field of such an enum is then its raw value, also with
--set-default-enum-member, and an enum that lists only null becomes None. An enum whose class has a value type
base, the IntEnum of a type: integer enum or a --use-subclass-enum class such as (int, Enum), cannot hold a
None member, so without the option it is written without that member and, as its one type rejects null,
without the alias.
When to use¶
- High-performance JSON/MessagePack serialization
- Memory-efficient data structures
- APIs with strict performance requirements
Choosing the Right Type¶
graph TD
A[Need runtime validation?] -->|Yes| B[Need best performance?]
A -->|No| C[Need type hints for dicts?]
B -->|Yes| D[msgspec.Struct]
B -->|No| G[Prefer dataclass syntax?]
G -->|Yes| H[pydantic_v2.dataclass]
G -->|No| I[pydantic_v2.BaseModel]
C -->|Yes| J[typing.TypedDict]
C -->|No| K[dataclasses.dataclass]
Decision Guide¶
- API with validation → Pydantic v2
- Validation with dataclass syntax → Pydantic v2 dataclass
- High-performance serialization → msgspec
- Simple data containers → dataclasses
- Dict-based JSON handling → TypedDict