Skip to content

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/json
  • tests/data/jsonschema/allof_ref_siblings (pydantic_v2.BaseModel): allOf contains a boolean false schema that makes the schema unsatisfiable
  • tests/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'] -> User
  • tests/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 package
  • tests/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 a
  • tests/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 name
  • tests/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 float
  • tests/data/jsonschema/msgspec_number_limits/float_range_bounds.json (msgspec.Struct): OverflowError: int too large to convert to float
  • tests/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 defined
  • tests/data/jsonschema/literal_constraints_undecided.yaml (dataclasses.dataclass): NameError: name 'inf' is not defined
  • tests/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_type
  • tests/data/jsonschema/allof_constraint_intersections/property_named_bounds.json (dataclasses.dataclass): For further information visit https://errors.pydantic.dev/2.14/v/float_type
  • tests/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 at field 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.