Output changes in 0.87.0¶
How the models 0.87.0 generates differ in behaviour from the models 0.86.0 generates. Both releases generated every test fixture and payload validation case of the repository with each output model type, with default options, with the released presets and with the documented CLI examples (tier release). Each generation (one input, output model type and option set) whose output differs was imported and compared by behaviour: public names, model kinds and bases, configuration, field types, constraints, aliases, defaults, required flags, enum members, constructor order, and the validation result of sample payloads.
Summary¶
| Output model type | same | no-behaviour | crash-fix | accepted | both-fail | opt-in |
|---|---|---|---|---|---|---|
| dataclasses.dataclass | 10302 | 28 | 318 | 7 | 667 | 20 |
| msgspec.Struct | 23581 | 44 | 1215 | 16 | 1708 | 22 |
| pydantic_v2.BaseModel | 23633 | 96 | 1135 | 138 | 1743 | 42 |
| pydantic_v2.dataclass | 10135 | 37 | 442 | 23 | 670 | 24 |
| typing.TypedDict | 8667 | 33 | 286 | 17 | 537 | 0 |
| Total | 76318 | 238 | 3396 | 201 | 5325 | 108 |
Generations per kind: documented CLI examples 244, default options 9530, payload validation cases 7196, released presets 68616.
- same: identical output
- no-behaviour: the output differs, the models behave the same (formatting, import order, redundant lines)
- crash-fix: the previous release failed to generate, import or validate; the new one works
- accepted: a behaviour change in default output that was decided and announced
- both-fail: both releases fail on the input
- opt-in: the generation uses an option or option value the previous release does not have
Fixed crashes¶
Generations that failed with 0.86.0 and work with 0.87.0, by the error 0.86.0 raised.
| Stage | Error | Generations | Inputs | Output model types |
|---|---|---|---|---|
| generation | no exception name | 1334 | 44 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass, typing.TypedDict |
| import | TypeError | 576 | 43 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass, typing.TypedDict |
| validation | no validator or unusable value | 318 | 34 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass, typing.TypedDict |
| import | ImportError | 205 | 5 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass, typing.TypedDict |
| import | ValueError | 160 | 11 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass |
| generation | OverflowError | 155 | 5 | msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass |
| import | NameError | 150 | 14 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass, typing.TypedDict |
| generation | ValidationError | 135 | 6 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass, typing.TypedDict |
| generation | AttributeError | 131 | 5 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass, typing.TypedDict |
| import | PydanticUserError | 105 | 9 | pydantic_v2.BaseModel, pydantic_v2.dataclass |
| generation | TypeError | 45 | 2 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass, typing.TypedDict |
| generation | NotADirectoryError | 41 | 1 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass, typing.TypedDict |
| import | OverflowError | 28 | 1 | msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass |
| import | AttributeError | 13 | 1 | pydantic_v2.BaseModel |
Generation, no exception name:
tests/data/jsonschema/allof_inherited_ref_other_directory(dataclasses.dataclass):$ref file not found: tests/data/jsontests/data/jsonschema/allof_ref_siblings(pydantic_v2.BaseModel):allOf contains a boolean false schema that makes the schema unsatisfiabletests/data/jsonschema/anchor_in_one_of.json(dataclasses.dataclass):Unresolved $id reference '#inner' in scope 'anchor_in_one_of.json'. Known $id values: <none>
Import, TypeError:
tests/data/jsonschema/discriminator_lone_scalar_variant.json(pydantic_v2.BaseModel):TypeError: The core schema type 'str' is not a valid discriminated union variant.tests/data/jsonschema/discriminator_tag_of_earlier_module(dataclasses.dataclass):TypeError: non-default argument 'kind' follows default argument 'name'tests/data/jsonschema/discriminator_tag_value_kinds.json(msgspec.Struct):TypeError: `tag` must be a `str` or an `int`
Validation, no validator or unusable value:
tests/data/asyncapi/edge-cases.yaml(pydantic_v2.dataclass):[type] UserAlias: Annotated[User, discriminator='kind'] -> Usertests/data/jsonschema/additional_pattern_intersections(msgspec.Struct):[type] anchored_disjoint:Root: Union[dict[str, Root1], dict[str, Root2]] -> dict[str, Union[Root1, Root2]]tests/data/jsonschema/annotated_string_keys(msgspec.Struct):[type] union:SomeSpec:services: Union[dict[str, int], dict[str, str]] -> dict[str, Union[int, str]]
Import, ImportError:
tests/data/openapi/allof_self_reference_base.yaml(dataclasses.dataclass):ImportError: attempted relative import with no known parent packagetests/data/openapi/circular_imports_module_below_package.yaml(dataclasses.dataclass):ImportError: cannot import name 'ReceivedDebit' from partially initialized module 'pkg._internal' (most likely due to atests/data/openapi/circular_imports_module_using_ancestor_package.yaml(dataclasses.dataclass):ImportError: cannot import name 'Item' from partially initialized module 'pkg._internal' (most likely due to a circular
Import, ValueError:
tests/data/jsonschema/discriminator_enum_const_outside_enum.json(msgspec.Struct):ValueError: `tag_field='k' conflicts with an existing field of that nametests/data/jsonschema/enum_value_base_kinds.json(dataclasses.dataclass):ValueError: invalid literal for int() with base 10: 'a'tests/data/jsonschema/msgspec_tag_below_instance_field.json(msgspec.Struct):ValueError: `tag_field='k' conflicts with an existing field of that name
Generation, OverflowError:
tests/data/jsonschema/msgspec_number_limits(msgspec.Struct):OverflowError: int too large to convert to floattests/data/jsonschema/msgspec_number_limits/float_range_bounds.json(msgspec.Struct):OverflowError: int too large to convert to floattests/data/jsonschema/numeric_constraint_precision(msgspec.Struct):OverflowError: int too large to convert to float
Import, NameError:
tests/data/jsonschema/annotated_forward_reference.json(typing.TypedDict):NameError: name 'Node' is not definedtests/data/jsonschema/literal_constraints_undecided.yaml(dataclasses.dataclass):NameError: name 'inf' is not definedtests/data/jsonschema/non_finite_enum_members.yaml(dataclasses.dataclass):NameError: name 'inf' is not defined
Generation, ValidationError:
tests/data/jsonschema/allof_constraint_intersections(dataclasses.dataclass):For further information visit https://errors.pydantic.dev/2.14/v/float_typetests/data/jsonschema/allof_constraint_intersections/property_named_bounds.json(dataclasses.dataclass):For further information visit https://errors.pydantic.dev/2.14/v/float_typetests/data/jsonschema/discriminator_tag_value_kinds.json(dataclasses.dataclass):For further information visit https://errors.pydantic.dev/2.14/v/string_type
Generation, AttributeError:
tests/data/openapi/allof_boolean_member(dataclasses.dataclass):AttributeError: 'bool' object has no attribute 'get'tests/data/openapi/allof_boolean_member/external.yaml(dataclasses.dataclass):AttributeError: 'bool' object has no attribute 'get'tests/data/openapi/allof_boolean_member/openapi.yaml(dataclasses.dataclass):AttributeError: 'bool' object has no attribute 'get'
Import, PydanticUserError:
tests/data/asyncapi/edge-cases.yaml(pydantic_v2.BaseModel):PydanticUserError: Model 'User' needs field 'kind' to be of type `Literal`tests/data/jsonschema/discriminator_lone_variant_modules(pydantic_v2.BaseModel):PydanticUserError: Model 'Untagged' needs a discriminator field for key 'type'tests/data/jsonschema/discriminator_lone_variant_modules/a.json(pydantic_v2.BaseModel):PydanticUserError: Model 'Untagged' needs a discriminator field for key 'type'
Generation, TypeError:
tests/data/jsonschema/allof_type_list_member.json(dataclasses.dataclass):TypeError: cannot use 'tuple' as a set element (unhashable type: 'list')payload:jsonschema/allof_type_list_member.json(dataclasses.dataclass,--target-python-version 3.14 --class-name Payload --disable-timestamp):TypeError: cannot use 'tuple' as a set element (unhashable type: 'list')
Generation, NotADirectoryError:
tests/data/asyncapi/avro-named-types.yaml(dataclasses.dataclass):NotADirectoryError: [Errno 20] Not a directory: '<path>
Import, OverflowError:
tests/data/jsonschema/enum_byte_counts_overflow.json(msgspec.Struct,--preset practical-py310-20261017):OverflowError: cannot fit 'int' into an index-sized integer
Import, AttributeError:
tests/data/jsonschema/property_names_root_model_key_modules(pydantic_v2.BaseModel):AttributeError: partially initialized module 'pkg.keys' from '<path>
Behaviour changes in default output¶
propertyNames keys are typed as constrained str instead of a RootModel that could not be hashed¶
- Decision: #5017
- Why: propertyNames dict keys RootModel -> constrained str: 0.86.0 keys are unhashable (crash class)
- Affected: 67 generations of 7 inputs; pydantic_v2.BaseModel; option sets:
--preset practical-py310-20261017,--preset practical-py312-20260619,--preset practical-py312-20260826,--preset practical-py312-20260909,--preset practical-py312-20261015,--preset practical-py314-20261017,--preset standard-py310-20261017,--preset standard-py312-20260619,--preset standard-py312-20260826,--preset standard-py312-20260909,--preset standard-py312-20261015,--preset standard-py314-20261017,default - Changed aspects: type, config
- How to keep the old output: no option restores it
tests/data/jsonschema/property_names_root_model_key_modules/z.json (pydantic_v2.BaseModel):
--- model.py
+++ model.py
@@ -8,5 +8,5 @@
class A(BaseModel):
- m: dict[Key, int] | None = None
+ m: dict[constr(max_length=3), int] | None = None
an external $anchor resolves to the anchored schema regardless of file read order¶
- Decision: #5088
- Why: spec-correct $anchor target; 0.86.0 depended on file read order (announced in the 0.87.0 release notes)
- Affected: 38 generations of 1 inputs; dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass, typing.TypedDict; option sets:
--preset practical-py310-20261017,--preset practical-py312-20260619,--preset practical-py312-20260826,--preset practical-py312-20260909,--preset practical-py312-20261015,--preset practical-py314-20261017,--preset standard-py310-20261017,--preset standard-py312-20260619,--preset standard-py312-20260826,--preset standard-py312-20260909,--preset standard-py312-20261015,--preset standard-py314-20261017,default - Changed aspects: type
- How to keep the old output: no option restores it
tests/data/jsonschema/external_anchor (dataclasses.dataclass):
--- pkg/root.py
+++ pkg/root.py
@@ -11,6 +11,6 @@
@dataclass
class ExternalAnchor:
- anchored: child.NestedChild | None = None
- anchored_again: child.NestedChild | None = None
+ anchored: child.AnchoredChild | None = None
+ anchored_again: child.AnchoredChild | None = None
legacy: child.LegacyChild | None = None
nested: child.NestedChild | None = None
string keywords are no longer written on date, time, UUID and other non-string types, where Pydantic raised TypeError on every value¶
- Decision: #5001
- Why: StampLenMap was RootModel[AwareDatetime] with max_length: TypeError on every value in 0.86.0 (crash class)
- Affected: 16 generations of 1 inputs; pydantic_v2.BaseModel, pydantic_v2.dataclass; option sets:
--preset practical-py310-20261017,--preset practical-py312-20260619,--preset practical-py312-20260826,--preset practical-py312-20260909,--preset practical-py312-20261015,--preset practical-py314-20261017,--preset standard-py310-20261017,--preset standard-py312-20260619,--preset standard-py312-20260826,--preset standard-py312-20260909,--preset standard-py312-20261015,--preset standard-py314-20261017 - Changed aspects: name-removed, type, constraints
- How to keep the old output: no option restores it
tests/data/jsonschema/schema_intersection/allof_refs.json (pydantic_v2.BaseModel, --preset practical-py310-20261017):
--- model.py
+++ model.py
@@ -21,8 +21,4 @@
class TwoRefsMap(RootModel[int]):
root: Annotated[int, Field(ge=3, le=5)]
-
-
-class StampLenMap(RootModel[AwareDatetime]):
- root: Annotated[AwareDatetime, Field(max_length=40)]
@@ -115,5 +111,5 @@
stamp_len_prop: AwareDatetime | None = None
stamp_len_items: list[StampLenItem] | None = None
- stamp_len_map: dict[str, StampLenMap] | None = None
+ stamp_len_map: dict[str, AwareDatetime] | None = None
quantity: Annotated[int | None, Field(ge=1)] = None
q_allof: Annotated[int | None, Field(ge=1, le=10)] = None
string keywords are no longer written on date, time, UUID and other non-string types, where Pydantic raised TypeError on every value¶
- Decision: #5001
- Why: StampMap was RootModel[AwareDatetime] with max_length: TypeError on every value in 0.86.0 (crash class)
- Affected: 16 generations of 1 inputs; pydantic_v2.BaseModel, pydantic_v2.dataclass; option sets:
--preset practical-py310-20261017,--preset practical-py312-20260619,--preset practical-py312-20260826,--preset practical-py312-20260909,--preset practical-py312-20261015,--preset practical-py314-20261017,--preset standard-py310-20261017,--preset standard-py312-20260619,--preset standard-py312-20260826,--preset standard-py312-20260909,--preset standard-py312-20261015,--preset standard-py314-20261017 - Changed aspects: name-removed, type, constraints
- How to keep the old output: no option restores it
tests/data/jsonschema/schema_intersection/allof_ref_item_constraints.json (pydantic_v2.BaseModel, --preset practical-py310-20261017):
--- model.py
+++ model.py
@@ -32,8 +32,4 @@
class TwoMap(RootModel[int]):
root: Annotated[int, Field(ge=4, le=5)]
-
-
-class StampMap(RootModel[AwareDatetime]):
- root: Annotated[AwareDatetime, Field(max_length=40)]
@@ -123,5 +119,5 @@
two_map: dict[str, TwoMap] | None = None
stamp_items: list[StampItem] | None = None
- stamp_map: dict[str, StampMap] | None = None
+ stamp_map: dict[str, AwareDatetime] | None = None
enum_items: list[EnumItem] | None = None
enum_map: dict[str, Quantity] | None = None
string keywords are no longer written on date, time, UUID and other non-string types, where Pydantic raised TypeError on every value¶
- Decision: #5001
- Why: every dropped keyword raised TypeError on every value in 0.86.0; the required
atfield made the model reject every payload (crash class) - Affected: 16 generations of 1 inputs; pydantic_v2.BaseModel, pydantic_v2.dataclass; option sets:
--preset practical-py310-20261017,--preset practical-py312-20260619,--preset practical-py312-20260826,--preset practical-py312-20260909,--preset practical-py312-20261015,--preset practical-py314-20261017,--preset standard-py310-20261017,--preset standard-py312-20260619,--preset standard-py312-20260826,--preset standard-py312-20260909,--preset standard-py312-20261015,--preset standard-py314-20261017 - Changed aspects: name-removed, type, constraints
- How to keep the old output: no option restores it
tests/data/jsonschema/string_constraints_on_emitted_types.json (pydantic_v2.BaseModel, --preset practical-py310-20261017):
--- model.py
+++ model.py
@@ -28,11 +28,5 @@
-class Day(RootModel[date]):
- root: Annotated[date, Field(max_length=10)]
-
-
-StampsAdditionalProperty = TypeAliasType(
- "StampsAdditionalProperty", Annotated[AwareDatetime, Field(min_length=20)]
-)
+StampsAdditionalProperty = TypeAliasType("StampsAdditionalProperty", AwareDatetime)
@@ -41,21 +35,21 @@
populate_by_name=True,
)
- at: Annotated[AwareDatetime, Field(max_length=64)]
- day: Annotated[date | None, Field(pattern='^2')] = None
- clock: Annotated[time | None, Field(min_length=5)] = None
- wait: Annotated[timedelta | None, Field(max_length=32)] = None
- id: Annotated[UUID | None, Field(min_length=36)] = None
- host: Annotated[IPv4Address | None, Field(max_length=15)] = None
- amount: Annotated[Decimal | None, Field(max_length=12)] = None
- tag: Annotated[UUID | None, Field(min_length=2)] = None
+ at: AwareDatetime
+ day: date | None = None
+ clock: time | None = None
+ wait: timedelta | None = None
+ id: UUID | None = None
+ host: IPv4Address | None = None
+ amount: Decimal | None = None
+ tag: UUID | None = None
link: AnyUrl | None = None
links: list[Link] | None = None
email: Annotated[EmailStr | None, Field(pattern='@example\\.com$')] = None
- secret: Annotated[SecretStr | None, Field(min_length=8, pattern='^[a-z]+$')] = None
+ secret: Annotated[SecretStr | None, Field(min_length=8)] = None
blob: Annotated[Base64Str | None, Field(max_length=12)] = None
... (8 more lines)
string keywords are no longer written on date, time, UUID and other non-string types, where Pydantic raised TypeError on every value¶
- Decision: #5001
- Why: required UUID fields with min_length raised TypeError on every value in 0.86.0, so Base and Child rejected every payload (crash class)
- Affected: 16 generations of 1 inputs; pydantic_v2.BaseModel, pydantic_v2.dataclass; option sets:
--preset practical-py310-20261017,--preset practical-py312-20260619,--preset practical-py312-20260826,--preset practical-py312-20260909,--preset practical-py312-20261015,--preset practical-py314-20261017,--preset standard-py310-20261017,--preset standard-py312-20260619,--preset standard-py312-20260826,--preset standard-py312-20260909,--preset standard-py312-20261015,--preset standard-py314-20261017 - Changed aspects: constraints
- How to keep the old output: no option restores it
tests/data/openapi/allof_no_merge_constraint_type_shape.yaml (pydantic_v2.BaseModel, --preset practical-py310-20261017):
--- model.py
+++ model.py
@@ -71,5 +71,5 @@
nullable_value: Annotated[str | None, Field(alias='nullableValue')]
non_nullable_value: Annotated[str | None, Field(alias='nonNullableValue')]
- ref_sibling_value: Annotated[UUID, Field(alias='refSiblingValue', min_length=2)]
+ ref_sibling_value: Annotated[UUID, Field(alias='refSiblingValue')]
unconstrained_union_value: Annotated[
str | Any, Field(alias='unconstrainedUnionValue')
@@ -172,5 +172,5 @@
enum_value: Annotated[Literal['ab'], Field(alias='enumValue')]
const_value: Annotated[Literal['ab'], Field(alias='constValue', min_length=1)]
- uuid_value: Annotated[UUID, Field(alias='uuidValue', min_length=1)]
+ uuid_value: Annotated[UUID, Field(alias='uuidValue')]
single_any_value: Annotated[str, Field(alias='singleAnyValue')]
single_one_value: Annotated[str, Field(alias='singleOneValue')]
@@ -195,5 +195,5 @@
nullable_value: Annotated[str | None, Field(alias='nullableValue', min_length=1)]
non_nullable_value: Annotated[str, Field(alias='nonNullableValue', min_length=1)]
- ref_sibling_value: Annotated[UUID, Field(alias='refSiblingValue', min_length=1)]
+ ref_sibling_value: Annotated[UUID, Field(alias='refSiblingValue')]
unconstrained_union_value: Annotated[
UnconstrainedUnionValue1 | Any, Field(alias='unconstrainedUnionValue')
string keywords are no longer written on date, time, UUID and other non-string types, where Pydantic raised TypeError on every value¶
- Decision: #5001
- Why: min_length on an exception class: no validator can be built in 0.86.0 (the model is not fully defined on both sides) (crash class)
- Affected: 2 generations of 1 inputs; pydantic_v2.BaseModel; option sets:
--preset practical-py314-20261017,--preset standard-py314-20261017 - Changed aspects: constraints
- How to keep the old output: no option restores it
tests/data/jsonschema/constraints_on_target_builtin_names.json (pydantic_v2.BaseModel, --preset practical-py314-20261017):
--- model.py
+++ model.py
@@ -15,5 +15,5 @@
frozendict_items: Annotated[frozendict, Field(max_length=3)]
import_cycle_minimum: Annotated[ImportCycleError, Field(ge=1)]
- finalization_length: Annotated[PythonFinalizationError, Field(min_length=2)]
+ finalization_length: PythonFinalizationError
finalization_minimum: PythonFinalizationError
exception_group_minimum: ExceptionGroup
a tuple item inlined by --collapse-root-models (practical presets) keeps its position instead of moving to the end¶
- Decision: #5291, #5283
- Why: tuple items in the wrong order rejected every schema-valid value in 0.86.0 (crash class)
- Affected: 18 generations of 1 inputs; dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass, typing.TypedDict; option sets:
--preset practical-py310-20261017,--preset practical-py312-20260619,--preset practical-py312-20260826,--preset practical-py312-20260909,--preset practical-py312-20261015,--preset practical-py314-20261017 - Changed aspects: type
- How to keep the old output: no option restores it
tests/data/jsonschema/collapse_root_models_tuple_items.json (dataclasses.dataclass, --preset practical-py310-20261017):
--- model.py
+++ model.py
@@ -9,4 +9,4 @@
@dataclass
class Model:
- pair: tuple[str, int | bool] | None = None
- triple: tuple[float, bool, str] | None = None
+ pair: tuple[int | bool, str] | None = None
+ triple: tuple[float, str, bool] | None = None
a tuple item inlined by --collapse-root-models (practical presets) keeps its position instead of moving to the end¶
- Decision: #5291, #5283
- Why: prefixItems [string, integer] was typed tuple[int, str] in 0.86.0: no valid value matches the annotation (crash class)
- Affected: 4 generations of 1 inputs; typing.TypedDict; option sets:
--preset practical-py310-20261017,--preset practical-py314-20261017,--preset standard-py310-20261017,--preset standard-py314-20261017 - Changed aspects: type
- How to keep the old output: no option restores it
tests/data/openapi/allof_no_merge_constraint_type_shape.yaml (pydantic_v2.BaseModel, --preset practical-py310-20261017):
--- model.py
+++ model.py
@@ -71,5 +71,5 @@
nullable_value: Annotated[str | None, Field(alias='nullableValue')]
non_nullable_value: Annotated[str | None, Field(alias='nonNullableValue')]
- ref_sibling_value: Annotated[UUID, Field(alias='refSiblingValue', min_length=2)]
+ ref_sibling_value: Annotated[UUID, Field(alias='refSiblingValue')]
unconstrained_union_value: Annotated[
str | Any, Field(alias='unconstrainedUnionValue')
@@ -172,5 +172,5 @@
enum_value: Annotated[Literal['ab'], Field(alias='enumValue')]
const_value: Annotated[Literal['ab'], Field(alias='constValue', min_length=1)]
- uuid_value: Annotated[UUID, Field(alias='uuidValue', min_length=1)]
+ uuid_value: Annotated[UUID, Field(alias='uuidValue')]
single_any_value: Annotated[str, Field(alias='singleAnyValue')]
single_one_value: Annotated[str, Field(alias='singleOneValue')]
@@ -195,5 +195,5 @@
nullable_value: Annotated[str | None, Field(alias='nullableValue', min_length=1)]
non_nullable_value: Annotated[str, Field(alias='nonNullableValue', min_length=1)]
- ref_sibling_value: Annotated[UUID, Field(alias='refSiblingValue', min_length=1)]
+ ref_sibling_value: Annotated[UUID, Field(alias='refSiblingValue')]
unconstrained_union_value: Annotated[
UnconstrainedUnionValue1 | Any, Field(alias='unconstrainedUnionValue')
a tuple item inlined by --collapse-root-models (practical presets) keeps its position instead of moving to the end¶
- Decision: #5291, #5283
- Why: prefixItems [string, integer] was typed tuple[int, str] in 0.86.0: no valid value matches the annotation (crash class)
- Affected: 4 generations of 1 inputs; typing.TypedDict; option sets:
--preset practical-py310-20261017,--preset practical-py314-20261017,--preset standard-py310-20261017,--preset standard-py314-20261017 - Changed aspects: type
- How to keep the old output: no option restores it
tests/data/openapi/allof_partial_unconstrained_schemas.yaml (typing.TypedDict, --preset practical-py310-20261017):
--- model.py
+++ model.py
@@ -36,7 +36,7 @@
scalarMappingWeaker: dict[str, str]
scalarDeepWeaker: list[list[str]]
- prefixItemsNeutral: tuple[int, str]
- legacyItemsNeutral: tuple[int, str]
- unevaluatedItemsNeutral: tuple[int, str]
+ prefixItemsNeutral: tuple[str, int]
+ legacyItemsNeutral: tuple[str, int]
+ unevaluatedItemsNeutral: tuple[str, int]
inlineObjectNeutral: Item
refObjectNeutral: Item
@@ -68,7 +68,7 @@
scalarMappingWeaker: dict[str, str]
scalarDeepWeaker: list[list[str]]
- prefixItemsNeutral: tuple[int, str]
- legacyItemsNeutral: tuple[int, str]
- unevaluatedItemsNeutral: tuple[int, str]
+ prefixItemsNeutral: tuple[str, int]
+ legacyItemsNeutral: tuple[str, int]
+ unevaluatedItemsNeutral: tuple[str, int]
inlineObjectNeutral: Item
refObjectNeutral: Item
a submodule named like a builtin (list, dict, ...) is imported under an alias, so the module's list[...] annotations name the builtin again¶
- Decision: #5387
- Why: the shadowing submodule import made Holder unusable (TypeError on every decode / type-hint evaluation) in 0.86.0 (crash class)
- Affected: 2 generations of 1 inputs; pydantic_v2.BaseModel; option sets:
--preset practical-py314-20261017,--preset standard-py314-20261017 - Changed aspects: import-path-removed
- How to keep the old output:
--all-exports-scope children,--all-exports-scope recursive,--enable-command-header,--enable-generated-header-marker,--enable-version-header,--no-use-standard-collections,--no-use-union-operator,--skip-root-model,--use-attribute-docstrings,--use-exact-imports,--use-generic-base-class,--use-generic-container-types
tests/data/jsonschema/module_import_named_like_builtin.json (pydantic_v2.BaseModel, --preset practical-py314-20261017):
--- pkg/holder.py
+++ pkg/holder.py
@@ -5,5 +5,5 @@
from . import app
-from .app import list
+from .app import list as list_1
@@ -13,4 +13,4 @@
)
tags: list[str] | None = None
- pop: list.Pop | None = None
+ pop: list_1.Pop | None = None
thing: app.Thing | None = None
TypedDict extra_items names the referenced class under its final name¶
- Decision: #5022
- Why: 0.86.0's extra_items named the local Bar instead of the referenced external Bar (renamed Bar1); a wrong reference is fixed by default
- Affected: 2 generations of 1 inputs; typing.TypedDict; option sets:
--preset practical-py310-20261017,--preset practical-py314-20261017 - Changed aspects: config
- How to keep the old output: no option restores it
tests/data/openapi/typed_dict_extra_items_final_name/renamed.yaml (typing.TypedDict, --preset practical-py310-20261017):
--- model.py
+++ model.py
@@ -15,4 +15,4 @@
-class Foo(TypedDict, extra_items='Bar'):
+class Foo(TypedDict, extra_items='Bar1'):
name: NotRequired[str]
New opt-in options¶
Options and option values 0.86.0 does not have. Default output does not change; each row shows where the option changes the output.
| Option | Generations | Inputs | Output model types |
|---|---|---|---|
--check-pattern-maps-strictly |
1 | 1 | pydantic_v2.BaseModel |
--collapse-shared-parent-wrappers |
1 | 1 | pydantic_v2.BaseModel |
--dedupe-request-response-variants |
1 | 1 | pydantic_v2.BaseModel |
--filter-literal-values |
5 | 2 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass |
--input-file-type malformed |
1 | 1 | pydantic_v2.BaseModel |
--intersect-allof-keywords |
9 | 3 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass |
--keep-enum-member-values |
9 | 3 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass |
--keep-field-defaults-off-branch-roots |
1 | 1 | pydantic_v2.BaseModel |
--omit-redundant-constraints |
3 | 2 | pydantic_v2.BaseModel, pydantic_v2.dataclass |
--omit-redundant-reference-null |
1 | 1 | pydantic_v2.BaseModel |
--openapi-as-definitions |
2 | 2 | pydantic_v2.BaseModel |
--order-constructor-fields-by |
2 | 2 | msgspec.Struct, pydantic_v2.dataclass |
--pin-aliases-with-alias-generator |
1 | 1 | pydantic_v2.dataclass |
--read-const-beside-enum |
49 | 13 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass |
--read-nullable-beside-type-list |
1 | 1 | pydantic_v2.BaseModel |
--read-unevaluated-properties |
1 | 1 | pydantic_v2.BaseModel |
--tag-msgspec-by-payload-name |
1 | 1 | msgspec.Struct |
--use-decimal-in-field-forms |
3 | 2 | pydantic_v2.BaseModel, pydantic_v2.dataclass |
--use-single-dict-for-object-maps |
5 | 2 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass |
--use-site-models-for-constrained-refs |
1 | 1 | pydantic_v2.BaseModel |
--use-tuple-for-closed-arrays |
9 | 3 | dataclasses.dataclass, msgspec.Struct, pydantic_v2.BaseModel, pydantic_v2.dataclass |
--use-typed-map-values-beside-other-types |
1 | 1 | pydantic_v2.BaseModel |
--check-pattern-maps-strictly¶
tests/data/jsonschema/object_maps/strict_pattern_maps.json (pydantic_v2.BaseModel), without and with --generate-schema-validators --check-pattern-maps-strictly:
--- model.py
+++ model.py
@@ -37,6 +37,5 @@
additional_adapter = None
for key, value in data.items():
- if key in rule['declared_properties']:
- continue
+ declared = key in rule['declared_properties']
if any(
re.search(pattern, key) for pattern in rule['rejected_patterns']
@@ -57,9 +56,11 @@
adapter = TypeAdapter(value_type)
pattern_adapters[index] = adapter
- value = adapter.validate_python(value)
+ validated = adapter.validate_python(value)
+ if declared:
+ continue
if matched:
if values is data:
values = dict(data)
- values[key] = value
+ values[key] = validated
continue
if rule['additional_property_type'] is not None:
@@ -106,5 +107,5 @@
)
- root: dict[str, Any] = Field(..., max_length=0, min_length=1)
+ root: dict[str, Any] = Field(..., min_length=1)
--collapse-shared-parent-wrappers¶
tests/data/openapi/collapse_root_models_name_strategy/later_holder_renames_wrapper.yaml (pydantic_v2.BaseModel), without and with --collapse-root-models --collapse-root-models-name-strategy parent --field-type-collision-strategy rename-type --collapse-shared-parent-wrappers:
--- model.py
+++ model.py
@@ -4,16 +4,12 @@
from __future__ import annotations
-from pydantic import BaseModel, RootModel
+from pydantic import BaseModel
-class Wrapper_2(BaseModel):
+class Wrapper_1(BaseModel):
x: str | None = None
-class Wrapper_1(RootModel[Wrapper_2]):
- root: Wrapper_2
-
-
class User(BaseModel):
- first: Wrapper_2 | None = None
+ first: Wrapper_1 | None = None
Wrapper: Wrapper_1 | None = None
--dedupe-request-response-variants¶
tests/data/openapi/read_only_write_only_allof_parent_inline_models.yaml (pydantic_v2.BaseModel), without and with --read-only-write-only-model-type request-response --dedupe-request-response-variants:
--- model.py
+++ model.py
@@ -29,13 +29,9 @@
-class Payload2(BaseModel):
- value: Value | None = None
-
-
class ParentRequest(BaseModel):
- payload: Payload2 | None = None
+ payload: Payload1 | None = None
class ParentResponse(BaseModel):
id: int | None = None
- payload: Payload2 | None = None
+ payload: Payload1 | None = None
--filter-literal-values¶
tests/data/openapi/max_items_enum.yaml (pydantic_v2.BaseModel), without and with --filter-literal-values:
--- model.py
+++ model.py
@@ -11,5 +11,4 @@
class BarEnum(Enum):
hello = 'hello'
- goodbye = 'goodbye'
--intersect-allof-keywords¶
tests/data/jsonschema/schema_intersection/inherited_overrides.json (pydantic_v2.BaseModel), without and with --intersect-allof-keywords:
--- model.py
+++ model.py
@@ -22,9 +22,8 @@
class E1(Enum):
b = 'b'
- c = 'c'
class Child(Base):
- x: conint(le=20) | None = None
+ x: conint(le=10) | None = None
e: E1 | None = None
y: conint(ge=0, le=5) | None = None
--keep-enum-member-values¶
tests/data/jsonschema/enum_value_base_kinds.json (pydantic_v2.BaseModel), without and with --keep-enum-member-values:
--- model.py
+++ model.py
@@ -19,5 +19,5 @@
-class IntThenFloat(IntEnum):
+class IntThenFloat(Enum):
integer_1 = 1
integer_2_5 = 2.5
--keep-field-defaults-off-branch-roots¶
tests/data/openapi/extracted_branch_default.yaml (pydantic_v2.BaseModel), without and with --field-constraints --keep-field-defaults-off-branch-roots:
--- model.py
+++ model.py
@@ -8,17 +8,17 @@
class Weight(RootModel[float]):
- root: float = Field(None, ge=0.0)
+ root: float = Field(..., ge=0.0)
class Quantity(RootModel[int]):
- root: int = Field(5, ge=0)
+ root: int = Field(..., ge=0)
class Quantity1(RootModel[str]):
- root: str = Field(5, max_length=3)
+ root: str = Field(..., max_length=3)
class Tags(RootModel[list[str]]):
- root: list[str] = Field(None, min_length=1)
+ root: list[str] = Field(..., min_length=1)
--omit-redundant-constraints¶
tests/data/jsonschema/redundant_constraints.json (pydantic_v2.BaseModel), without and with --use-annotated --use-non-positive-negative-number-constrained-types --use-single-value-enum-as-const --omit-redundant-constraints:
--- model.py
+++ model.py
@@ -18,5 +18,5 @@
class Count(RootModel[NonNegativeInt | None]):
- root: Annotated[NonNegativeInt | None, Field(ge=0)]
+ root: NonNegativeInt | None
@@ -25,18 +25,6 @@
-class Model1(RootModel[Literal['gpt-live-1']]):
- root: Annotated[Literal['gpt-live-1'], Field(min_length=1)]
-
-
class ConstModel(RootModel[str]):
root: Annotated[str, Field(min_length=1)]
-
-
-class ConstModel1(RootModel[Literal['gpt-live-1']]):
- root: Annotated[Literal['gpt-live-1'], Field(min_length=1)]
-
-
-class Code(RootModel[Literal['abc']]):
- root: Annotated[Literal['abc'], Field(min_length=2)]
@@ -45,22 +33,18 @@
-class PatternedItem(RootModel[Literal['abc']]):
- root: Annotated[Literal['abc'], Field(min_length=1, pattern='^a')]
-
-
class CountModel(RootModel[NonNegativeInt]):
- root: Annotated[NonNegativeInt, Field(ge=0)]
+ root: NonNegativeInt
... (62 more lines)
--omit-redundant-reference-null¶
tests/data/openapi/redundant_reference_null.yaml (pydantic_v2.BaseModel), without and with --strict-nullable --omit-redundant-reference-null:
--- model.py
+++ model.py
@@ -28,8 +28,8 @@
class Use(BaseModel):
- name_f: Name | None
- name_a: list[Name | None]
- name_m: dict[str, Name | None]
- name_u: Name | int | None
+ name_f: Name
+ name_a: list[Name]
+ name_m: dict[str, Name]
+ name_u: Name | int
name_o: Name | None
name_d: Name | None = None
--openapi-as-definitions¶
tests/data/openapi/ref_siblings_recursive_target/schema.yaml (pydantic_v2.BaseModel), without and with --openapi-as-definitions ref-components:
--- model.py
+++ model.py
@@ -4,9 +4,5 @@
from __future__ import annotations
-from pydantic import BaseModel, RootModel
-
-
-class Node(BaseModel):
- children: list[Node] | None = None
+from pydantic import BaseModel, Field, RootModel
@@ -19,22 +15,38 @@
-class Branch(Node):
+class Node(BaseModel):
+ children: list[Tree] | None = None
+
+
+class Holder(BaseModel):
+ tree: Tree | None = None
+ trees: list[Tree] | None = None
+ grove: Grove | None = None
+ either: Tree | str | None = None
+ byName: dict[str, Tree] | None = None
+ shrub: Shrub | None = None
+
+
+class Tree(RootModel[Node]):
+ root: Node = Field(..., description='A tree.')
+
+
+class Forest(RootModel[Node]):
+ root: Node
+
+
+class Grove(RootModel[Tree]):
+ root: Tree
... (26 more lines)
--order-constructor-fields-by¶
tests/data/jsonschema/field_type_collision_inherited_modules (pydantic_v2.dataclass), without and with --target-python-version 3.11 --order-constructor-fields-by final-names:
--- pkg/b.py
+++ pkg/b.py
@@ -13,4 +13,4 @@
@dataclass
class Child(Base):
- Bar_1: a.Bar = Field(..., alias='Bar', kw_only=True)
+ Bar_1: a.Bar = Field(..., alias='Bar')
Zed: str | None = None
--pin-aliases-with-alias-generator¶
tests/data/jsonschema/alias_generator.json (pydantic_v2.dataclass), without and with --snake-case-field --pin-aliases-with-alias-generator --extra-template-data jsonschema/extra_data_alias_generator.json --target-pydantic-version 2:
--- model.py
+++ model.py
@@ -10,7 +10,7 @@
@dataclass
class AliasGeneratorModel:
- first_name: str
- foo_bar: str = Field(..., alias='foo_bar')
- last_name: str | None = None
+ foo_bar: str
+ first_name: str = Field(..., alias='firstName')
+ last_name: str | None = Field(None, alias='lastName')
weird_url: int = Field(
..., serialization_alias='weird-url', validation_alias=AliasChoices('weird-url')
--read-const-beside-enum¶
tests/data/jsonschema/enum_const_positions.json (pydantic_v2.BaseModel), without and with --read-const-beside-enum:
--- model.py
+++ model.py
@@ -82,8 +82,8 @@
propertySingle: Literal['a'] | None = None
words: list[Word] | None = None
- counts: dict[str, Counts] | None = None
+ counts: dict[str, Literal[Counts.integer_2]] | None = None
branch: Branch | int | None = None
refSibling: Literal['on'] | None = None
- keys: dict[Literal['a', 'b'], int] | None = None
+ keys: dict[Literal['a'], int] | None = None
refKeys: dict[Narrowed, int] | None = None
definition: Narrowed | None = None
--read-nullable-beside-type-list¶
tests/data/openapi/schema_intersection/null_multiple_types_oas30.yaml (pydantic_v2.BaseModel), without and with --use-missing-sentinel --read-nullable-beside-type-list:
--- model.py
+++ model.py
@@ -12,6 +12,6 @@
-class U1(RootModel[U | str | MISSING]):
- root: U | str | MISSING = MISSING
+class U1(RootModel[U | str | None | MISSING]):
+ root: U | str | None | MISSING = MISSING
--read-unevaluated-properties¶
tests/data/jsonschema/unevaluated_properties_schema.json (pydantic_v2.BaseModel), without and with --read-unevaluated-properties:
--- model.py
+++ model.py
@@ -3,4 +3,6 @@
from __future__ import annotations
+
+from typing import Dict
from pydantic import BaseModel, ConfigDict
@@ -11,3 +13,6 @@
extra='allow',
)
+ __annotations__ = {
+ '__pydantic_extra__': Dict[str, str],
+ }
name: str
--tag-msgspec-by-payload-name¶
tests/data/jsonschema/discriminator_msgspec_wire_name.json (msgspec.Struct), without and with --tag-msgspec-by-payload-name:
--- model.py
+++ model.py
@@ -7,9 +7,9 @@
-class Cat(Struct, tag_field='pet_kind', tag='cat'):
+class Cat(Struct, tag_field='pet-kind', tag='cat'):
name: str | UnsetType = UNSET
-class Dog(Struct, tag_field='pet_kind', tag='dog'):
+class Dog(Struct, tag_field='pet-kind', tag='dog'):
bark: str | UnsetType = UNSET
--use-decimal-in-field-forms¶
tests/data/jsonschema/decimal_multiple_of_field_forms.json (pydantic_v2.BaseModel), without and with --use-decimal-for-multiple-of --use-annotated --use-decimal-in-field-forms:
--- model.py
+++ model.py
@@ -4,4 +4,5 @@
from __future__ import annotations
+from decimal import Decimal
from typing import Annotated
@@ -10,26 +11,24 @@
-class ArrItem(RootModel[float]):
- root: Annotated[float, Field(ge=-1000000.0, le=1000000.0, multiple_of=0.01)]
+class ArrItem(RootModel[Decimal]):
+ root: Annotated[Decimal, Field(ge=-1000000, le=1000000, multiple_of=0.01)]
MapvAdditionalProperty = TypeAliasType(
"MapvAdditionalProperty",
- Annotated[float, Field(ge=-1000000.0, le=1000000.0, multiple_of=0.01)],
+ Annotated[Decimal, Field(ge=-1000000, le=1000000, multiple_of=0.01)],
)
-class Rate(RootModel[float]):
- root: Annotated[float, Field(gt=0.0, le=1000000.0, multiple_of=0.001)]
+class Rate(RootModel[Decimal]):
+ root: Annotated[Decimal, Field(gt=0, le=1000000, multiple_of=0.001)]
class DecimalMultipleOfFieldForms(BaseModel):
- amount: Annotated[float, Field(ge=0.0, le=1000000.0, multiple_of=0.01)]
- opt: Annotated[
- float | None, Field(ge=-1000000.0, le=1000000.0, multiple_of=0.5)
- ] = None
- maxed: Annotated[float | None, Field(ge=-1000000.0, le=10.0, multiple_of=0.1)] = (
+ amount: Annotated[Decimal, Field(ge=0, le=1000000, multiple_of=0.01)]
+ opt: Annotated[Decimal | None, Field(ge=-1000000, le=1000000, multiple_of=0.5)] = (
None
)
... (3 more lines)
--use-single-dict-for-object-maps¶
tests/data/jsonschema/pattern_properties_merge.json (pydantic_v2.BaseModel), without and with --use-single-dict-for-object-maps:
--- model.py
+++ model.py
@@ -7,6 +7,4 @@
-class Model(
- RootModel[dict[constr(pattern=r'^i-|^t-'), str] | dict[constr(pattern=r'^n-'), int]]
-):
- root: dict[constr(pattern=r'^i-|^t-'), str] | dict[constr(pattern=r'^n-'), int]
+class Model(RootModel[dict[constr(pattern=r'^i-|^t-|^n-'), str | int]]):
+ root: dict[constr(pattern=r'^i-|^t-|^n-'), str | int]
--use-site-models-for-constrained-refs¶
tests/data/jsonschema/site_models_for_constrained_refs.json (pydantic_v2.BaseModel), without and with --use-site-models-for-constrained-refs:
--- model.py
+++ model.py
@@ -9,4 +9,18 @@
+class Quantity1(RootModel[conint(ge=1, le=10)]):
+ root: conint(ge=1, le=10)
+
+
+class Size2(Enum):
+ m = 'm'
+ l = 'l'
+
+
+class Order(BaseModel):
+ quantity: Quantity1 | None = None
+ sizes: list[Size2] | None = None
+
+
class Quantity(RootModel[conint(ge=1)]):
root: conint(ge=1)
@@ -17,7 +31,2 @@
m = 'm'
l = 'l'
-
-
-class Order(BaseModel):
- quantity: Quantity | None = None
- sizes: list[Size] | None = None
--use-tuple-for-closed-arrays¶
tests/data/jsonschema/prefix_items_closed_length.json (pydantic_v2.BaseModel), without and with --use-tuple-for-closed-arrays:
--- model.py
+++ model.py
@@ -22,5 +22,5 @@
class Parent(BaseModel):
- closed: list[str | int] | None = Field(None, max_length=2, min_length=2)
+ closed: tuple[str, int] | None = None
@@ -30,29 +30,17 @@
class Child(Parent):
- closed: list[ClosedItem | int] | None = Field(None, max_length=2, min_length=2)
+ closed: tuple[ClosedItem, int] | None = None
class PrefixItemsClosedLength(BaseModel):
- closed_by_items_false: list[str | int] | None = Field(
- None, max_length=2, min_length=2
- )
- closed_by_unevaluated_items_false: list[str | int] | None = Field(
- None, max_length=2, min_length=2
- )
- closed_by_false_position: list[str | int] | None = Field(
- None, max_length=2, min_length=2
- )
- closed_below_max_items: list[str | int] | None = Field(
- None, max_length=2, min_length=2
- )
- closed_nullable: list[str | int] | None = Field(None, max_length=2, min_length=2)
- closed_by_contains: list[Any] | None = Field(None, max_length=2, min_length=2)
- closed_by_contains_beyond_positions: list[Any] | None = Field(
- None, max_length=3, min_length=3
- )
- closed_to_one_position: list[Any] | None = Field(None, max_length=1, min_length=1)
- closed_before_other_positions: list[str | int] | None = Field(
- None, max_length=2, min_length=2
- )
+ closed_by_items_false: tuple[str, int] | None = None
+ closed_by_unevaluated_items_false: tuple[str, int] | None = None
... (16 more lines)
--use-typed-map-values-beside-other-types¶
tests/data/jsonschema/typed_map_values_beside_other_types.json (pydantic_v2.BaseModel), without and with --use-typed-nullable-map-values --use-typed-map-values-beside-other-types:
--- model.py
+++ model.py
@@ -4,11 +4,13 @@
from __future__ import annotations
-from typing import Any
+from pydantic import BaseModel
-from pydantic import BaseModel
+
+class Owners(BaseModel):
+ name: str | None = None
class Settings(BaseModel):
limits: dict[str, int] | None = None
- quotas: dict[str, Any] | str | None = None
- owners: dict[str, Any] | str | None = None
+ quotas: dict[str, int] | str | None = None
+ owners: dict[str, Owners] | str | None = None
Upgrading across several releases¶
Upgrading from an older release compares the recorded older releases with the latest one.