Initial Checks
Description
Pydantic Bug Report: Subclass Serialization Includes Child Fields with MISSING Sentinel
Summary
When a base class contains fields with | MISSING type annotation AND a container field uses | MISSING in its union type, Pydantic fails to properly serialize subclasses according to their declared parent type. Instead, it serializes the full runtime (child) type, including fields that should be excluded.
Versions
- pydantic: 2.12.5
- pydantic_core: 2.41.5
Minimal Reproducer
from pydantic import BaseModel, ConfigDict, TypeAdapter
from pydantic.experimental.missing_sentinel import MISSING
class Base(BaseModel):
model_config = ConfigDict(extra='forbid')
base_field: str | MISSING = MISSING # Key ingredient 1
class Parent(Base):
parent_field: str
class Child(Parent):
child_field: str # Should NOT appear when serializing as Parent
class Container(BaseModel):
model_config = ConfigDict(extra='forbid')
item: Parent | MISSING = MISSING # Key ingredient 2
# Create child and assign to parent-typed field
child = Child(parent_field='p', child_field='c')
container = Container(item=child)
# Serialize
result = TypeAdapter(Container).dump_python(container, mode='json')
# Expected: {'item': {'parent_field': 'p'}}
# Actual: {'item': {'parent_field': 'p', 'child_field': 'c'}}
print(f"Has child_field (BUG): {'child_field' in result.get('item', {})}") # True (incorrect)
Control Tests
The issue only occurs when both conditions are met:
Without MISSING in base class - Works correctly
class BaseNoMissing(BaseModel):
model_config = ConfigDict(extra='forbid')
base_field: str = '' # Not MISSING
class ParentNoMissing(BaseNoMissing):
parent_field: str
class ChildNoMissing(ParentNoMissing):
child_field: str
class ContainerControl1(BaseModel):
model_config = ConfigDict(extra='forbid')
item: ParentNoMissing | MISSING = MISSING
child1 = ChildNoMissing(parent_field='p', child_field='c')
container1 = ContainerControl1(item=child1)
result1 = TypeAdapter(ContainerControl1).dump_python(container1, mode='json')
print(f"Without MISSING in base: has child_field = {'child_field' in result1.get('item', {})}") # False (correct)
Without MISSING in container field union - Works correctly
class ContainerControl2(BaseModel):
model_config = ConfigDict(extra='forbid')
item: Parent # No MISSING union
child2 = Child(parent_field='p', child_field='c')
container2 = ContainerControl2(item=child2)
result2 = TypeAdapter(ContainerControl2).dump_python(container2, mode='json')
print(f"Without MISSING in container: has child_field = {'child_field' in result2.get('item', {})}") # False (correct)
Using None instead of MISSING - Works correctly
class ContainerWithNone(BaseModel):
model_config = ConfigDict(extra='forbid')
item: Parent | None = None
child3 = Child(parent_field='p', child_field='c')
container3 = ContainerWithNone(item=child3)
result3 = TypeAdapter(ContainerWithNone).dump_python(container3, mode='json')
print(f"With None instead of MISSING: has child_field = {'child_field' in result3.get('item', {})}") # False (correct)
Expected Behavior
When serializing a child class instance through a field typed as the parent class, Pydantic should only serialize fields defined in the declared type (parent), excluding fields added by subclasses.
Actual Behavior
When both a base class in the inheritance chain has | MISSING fields AND the container field uses | MISSING union, Pydantic serializes all fields from the runtime type, including child-specific fields that should be excluded.
Impact
This breaks type safety in APIs where the response schema declares a parent type but the implementation may return subclasses. Clients receive unexpected extra fields that violate the API contract.
Workaround
Replace | MISSING with | None in the container field type annotation:
class Container(BaseModel):
item: Parent | None = None # Instead of: Parent | MISSING = MISSING
This changes the API semantics slightly (returns null instead of omitting the field when unset), but fixes the serialization bug.
Root Cause Analysis
The combination of MISSING in both the base class fields and the container union appears to break Pydantic's serialization schema generation. The serializer falls back to using the runtime type rather than the declared type annotation.
When serializing directly with TypeAdapter(TicketShort) on the child instance, it correctly excludes child fields. The issue only manifests when serializing through a container with the problematic | MISSING union pattern.
Example Code
Python, Pydantic & OS Version
pydantic version: 2.12.5
pydantic-core version: 2.41.5
pydantic-core build: profile=release pgo=false
python version: 3.13.11 (main, Feb 2 2026, 19:03:49) [Clang 17.0.0 (clang-1700.3.19.1)]
platform: macOS-26.3.1-arm64-arm-64bit-Mach-O
related packages: fastapi-0.128.6 mypy-1.19.1 email-validator-2.3.0 typing_extensions-4.15.0
commit: unknown
Initial Checks
Description
Pydantic Bug Report: Subclass Serialization Includes Child Fields with MISSING Sentinel
Summary
When a base class contains fields with
| MISSINGtype annotation AND a container field uses| MISSINGin its union type, Pydantic fails to properly serialize subclasses according to their declared parent type. Instead, it serializes the full runtime (child) type, including fields that should be excluded.Versions
Minimal Reproducer
Control Tests
The issue only occurs when both conditions are met:
Without MISSING in base class - Works correctly
Without MISSING in container field union - Works correctly
Using None instead of MISSING - Works correctly
Expected Behavior
When serializing a child class instance through a field typed as the parent class, Pydantic should only serialize fields defined in the declared type (parent), excluding fields added by subclasses.
Actual Behavior
When both a base class in the inheritance chain has
| MISSINGfields AND the container field uses| MISSINGunion, Pydantic serializes all fields from the runtime type, including child-specific fields that should be excluded.Impact
This breaks type safety in APIs where the response schema declares a parent type but the implementation may return subclasses. Clients receive unexpected extra fields that violate the API contract.
Workaround
Replace
| MISSINGwith| Nonein the container field type annotation:This changes the API semantics slightly (returns
nullinstead of omitting the field when unset), but fixes the serialization bug.Root Cause Analysis
The combination of
MISSINGin both the base class fields and the container union appears to break Pydantic's serialization schema generation. The serializer falls back to using the runtime type rather than the declared type annotation.When serializing directly with
TypeAdapter(TicketShort)on the child instance, it correctly excludes child fields. The issue only manifests when serializing through a container with the problematic| MISSINGunion pattern.Example Code
Python, Pydantic & OS Version