# pydantic-discriminated
A robust, type-safe implementation of discriminated unions for Pydantic models.
[](https://pypi.org/project/pydantic-discriminated/)
[](https://pypi.org/project/pydantic-discriminated/)
[](https://github.com/TalbotKnighton/pydantic-discriminated/blob/main/LICENSE)
[](https://talbotknighton.github.io/pydantic-discriminated/)
[](https://github.com/TalbotKnighton/pydantic-discriminated)
## What are Discriminated Unions?
Discriminated unions (also called tagged unions) let you work with polymorphic data in a type-safe way. A "discriminator" field tells you which concrete type you're dealing with.
## Installation
```bash
pip install pydantic-discriminated
```
## Quick Start
```python
from enum import Enum
from typing import List, Union
from pydantic import BaseModel
from pydantic_discriminated import discriminated_model, DiscriminatedBaseModel
# Define discriminated models with their tag values
@discriminated_model("shape_type", "circle")
class Circle(DiscriminatedBaseModel):
radius: float
def area(self) -> float:
return 3.14159 * self.radius ** 2
@discriminated_model("shape_type", "rectangle")
class Rectangle(DiscriminatedBaseModel):
width: float
height: float
def area(self) -> float:
return self.width * self.height
# Container for shapes
class ShapeCollection(BaseModel):
shapes: List[Union[Circle, Rectangle]]
def total_area(self) -> float:
return sum(shape.area() for shape in self.shapes)
# Parse polymorphic data correctly
data = {
"shapes": [
{"shape_type": "circle", "radius": 5},
{"shape_type": "rectangle", "width": 10, "height": 20}
]
}
shapes = ShapeCollection.model_validate(data)
print(f"Total area: {shapes.total_area()}") # 278.5795
# Each shape is properly typed
for shape in shapes.shapes:
if isinstance(shape, Circle):
print(f"Circle with radius {shape.radius}")
elif isinstance(shape, Rectangle):
print(f"Rectangle with dimensions {shape.width}x{shape.height}")
```
## Key Features
- **🔍 Type Safety**: Proper type hints for IDE autocomplete and static analysis
- **📦 Nested Models**: Works with models nested at any level
- **🔄 Seamless Integration**: Uses standard Pydantic methods (`model_validate`, `model_dump`)
- **🧩 Polymorphic Validation**: Automatically validates and dispatches to the correct model type
- **📚 OpenAPI Compatible**: Works great with FastAPI for generating correct schemas
- **🔧 Flexible Configuration**: Control when and how discriminator fields are included in serialization
## How It Works
Under the hood, pydantic-discriminated uses several advanced techniques to provide its functionality:
1. **Model Registration**: The `@discriminated_model` decorator registers models in a central registry with their discriminator field and value
2. **Custom Model Serialization**: Enhances Pydantic's serialization to properly handle discriminator fields
3. **Monkey Patching (Optional)**: Can patch Pydantic's BaseModel to handle discriminators in nested models automatically
4. **Type Annotations**: Preserves type information for static analyzers and IDEs
## Flexible Serialization
You can control when discriminator fields are included in serialized output:
```python
from pydantic_discriminated import DiscriminatedConfig
# Global configuration - include discriminators when serializing
DiscriminatedConfig.enable_monkey_patching()
# Serialize with discriminators
shape = Circle(radius=5)
data = shape.model_dump() # Will include 'shape_type': 'circle'
# Disable discriminators globally
DiscriminatedConfig.disable_monkey_patching()
# Now serialization won't include discriminators by default
data = shape.model_dump() # Won't include 'shape_type'
# Override per-call behavior
data = shape.model_dump(use_discriminators=True) # Will include 'shape_type': 'circle'
```
## Two Usage Approaches
### 1. Automatic Monkey Patching (Simple)
This approach patches Pydantic's BaseModel to automatically include discriminator fields:
```python
from pydantic_discriminated import discriminated_model, DiscriminatedBaseModel, DiscriminatedConfig
# Enable monkey patching (default)
DiscriminatedConfig.enable_monkey_patching()
@discriminated_model("shape_type", "circle")
class Circle(DiscriminatedBaseModel):
radius: float
# Regular BaseModel containers work automatically
class ShapeContainer(BaseModel):
shape: Circle
container = ShapeContainer(shape=Circle(radius=5))
data = container.model_dump() # Will include shape_type automatically
```
### 2. Explicit Base Class (Advanced)
For more control, you can use the DiscriminatorAwareBaseModel for your containers:
```python
from pydantic_discriminated import (
discriminated_model, DiscriminatedBaseModel,
DiscriminatorAwareBaseModel, DiscriminatedConfig
)
# Disable monkey patching
DiscriminatedConfig.disable_monkey_patching()
@discriminated_model("shape_type", "circle")
class Circle(DiscriminatedBaseModel):
radius: float
# Use DiscriminatorAwareBaseModel for containers
class ShapeContainer(DiscriminatorAwareBaseModel):
shape: Circle
container = ShapeContainer(shape=Circle(radius=5))
data = container.model_dump() # Will include shape_type
```
## Advanced Usage
### Enum Discriminators
```python
from enum import Enum
from pydantic_discriminated import discriminated_model, DiscriminatedBaseModel
class MessageType(str, Enum):
TEXT = "text"
IMAGE = "image"
VIDEO = "video"
@discriminated_model(MessageType, MessageType.TEXT)
class TextMessage(DiscriminatedBaseModel):
content: str
@discriminated_model(MessageType, MessageType.IMAGE)
class ImageMessage(DiscriminatedBaseModel):
url: str
width: int
height: int
```
### Standard Fields
By default, discriminator fields are included both as domain-specific fields (e.g., `shape_type`) and as standard fields for interoperability:
```python
circle = Circle(radius=5)
data = circle.model_dump()
# Results in:
# {
# "radius": 5,
# "shape_type": "circle", # Domain-specific discriminator
# "discriminator_category": "shape_type", # Standard category field
# "discriminator_value": "circle" # Standard value field
# }
```
You can control this behavior globally or per-model:
```python
# Global configuration
DiscriminatedConfig.use_standard_fields = False
# Per-model configuration using model_config
@discriminated_model("animal_type", "cat")
class Cat(DiscriminatedBaseModel):
model_config = {"use_standard_fields": False}
name: str
lives_left: int
# Direct parameter in decorator
@discriminated_model("animal_type", "dog", use_standard_fields=True)
class Dog(DiscriminatedBaseModel):
name: str
breed: str
```
## FastAPI Example
```python
from fastapi import FastAPI
from typing import Union, List
app = FastAPI()
@app.post("/shapes/")
def process_shape(shape: Union[Circle, Rectangle]):
return {"area": shape.area()}
@app.post("/shape-collection/")
def process_shapes(shapes: ShapeCollection):
return {"total_area": shapes.total_area()}
```
This will automatically generate the correct OpenAPI schema with discriminator support!
## License
MIT
---
This library fills a significant gap in Pydantic's functionality. If you work with polymorphic data structures, it will make your life easier!
Raw data
{
"_id": null,
"home_page": null,
"name": "pydantic-discriminated",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": "Talbot Knighton <talbotknighton@gmail.com>",
"keywords": "discriminated, polymorphic, pydantic, serialization, unions, validation",
"author": null,
"author_email": "Talbot Knighton <talbotknighton@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/71/4c/ecc69d4c28699260cb1875dca37207209c06a0a369f451393fd8ebaf167e/pydantic_discriminated-0.1.28.tar.gz",
"platform": null,
"description": "# pydantic-discriminated\n\nA robust, type-safe implementation of discriminated unions for Pydantic models.\n\n[](https://pypi.org/project/pydantic-discriminated/)\n[](https://pypi.org/project/pydantic-discriminated/)\n[](https://github.com/TalbotKnighton/pydantic-discriminated/blob/main/LICENSE)\n[](https://talbotknighton.github.io/pydantic-discriminated/)\n[](https://github.com/TalbotKnighton/pydantic-discriminated)\n\n## What are Discriminated Unions?\n\nDiscriminated unions (also called tagged unions) let you work with polymorphic data in a type-safe way. A \"discriminator\" field tells you which concrete type you're dealing with.\n\n## Installation\n\n```bash\npip install pydantic-discriminated\n```\n\n## Quick Start\n\n```python\nfrom enum import Enum\nfrom typing import List, Union\nfrom pydantic import BaseModel\nfrom pydantic_discriminated import discriminated_model, DiscriminatedBaseModel\n\n# Define discriminated models with their tag values\n@discriminated_model(\"shape_type\", \"circle\")\nclass Circle(DiscriminatedBaseModel):\n radius: float\n \n def area(self) -> float:\n return 3.14159 * self.radius ** 2\n\n@discriminated_model(\"shape_type\", \"rectangle\")\nclass Rectangle(DiscriminatedBaseModel):\n width: float\n height: float\n \n def area(self) -> float:\n return self.width * self.height\n\n# Container for shapes\nclass ShapeCollection(BaseModel):\n shapes: List[Union[Circle, Rectangle]]\n \n def total_area(self) -> float:\n return sum(shape.area() for shape in self.shapes)\n\n# Parse polymorphic data correctly\ndata = {\n \"shapes\": [\n {\"shape_type\": \"circle\", \"radius\": 5},\n {\"shape_type\": \"rectangle\", \"width\": 10, \"height\": 20}\n ]\n}\nshapes = ShapeCollection.model_validate(data)\nprint(f\"Total area: {shapes.total_area()}\") # 278.5795\n\n# Each shape is properly typed\nfor shape in shapes.shapes:\n if isinstance(shape, Circle):\n print(f\"Circle with radius {shape.radius}\")\n elif isinstance(shape, Rectangle):\n print(f\"Rectangle with dimensions {shape.width}x{shape.height}\")\n```\n\n## Key Features\n\n- **\ud83d\udd0d Type Safety**: Proper type hints for IDE autocomplete and static analysis\n- **\ud83d\udce6 Nested Models**: Works with models nested at any level\n- **\ud83d\udd04 Seamless Integration**: Uses standard Pydantic methods (`model_validate`, `model_dump`)\n- **\ud83e\udde9 Polymorphic Validation**: Automatically validates and dispatches to the correct model type\n- **\ud83d\udcda OpenAPI Compatible**: Works great with FastAPI for generating correct schemas\n- **\ud83d\udd27 Flexible Configuration**: Control when and how discriminator fields are included in serialization\n\n## How It Works\n\nUnder the hood, pydantic-discriminated uses several advanced techniques to provide its functionality:\n\n1. **Model Registration**: The `@discriminated_model` decorator registers models in a central registry with their discriminator field and value\n2. **Custom Model Serialization**: Enhances Pydantic's serialization to properly handle discriminator fields\n3. **Monkey Patching (Optional)**: Can patch Pydantic's BaseModel to handle discriminators in nested models automatically\n4. **Type Annotations**: Preserves type information for static analyzers and IDEs\n\n## Flexible Serialization\n\nYou can control when discriminator fields are included in serialized output:\n\n```python\nfrom pydantic_discriminated import DiscriminatedConfig\n\n# Global configuration - include discriminators when serializing\nDiscriminatedConfig.enable_monkey_patching()\n\n# Serialize with discriminators\nshape = Circle(radius=5)\ndata = shape.model_dump() # Will include 'shape_type': 'circle'\n\n# Disable discriminators globally\nDiscriminatedConfig.disable_monkey_patching()\n\n# Now serialization won't include discriminators by default\ndata = shape.model_dump() # Won't include 'shape_type'\n\n# Override per-call behavior\ndata = shape.model_dump(use_discriminators=True) # Will include 'shape_type': 'circle'\n```\n\n## Two Usage Approaches\n\n### 1. Automatic Monkey Patching (Simple)\n\nThis approach patches Pydantic's BaseModel to automatically include discriminator fields:\n\n```python\nfrom pydantic_discriminated import discriminated_model, DiscriminatedBaseModel, DiscriminatedConfig\n\n# Enable monkey patching (default)\nDiscriminatedConfig.enable_monkey_patching()\n\n@discriminated_model(\"shape_type\", \"circle\")\nclass Circle(DiscriminatedBaseModel):\n radius: float\n\n# Regular BaseModel containers work automatically\nclass ShapeContainer(BaseModel):\n shape: Circle\n\ncontainer = ShapeContainer(shape=Circle(radius=5))\ndata = container.model_dump() # Will include shape_type automatically\n```\n\n### 2. Explicit Base Class (Advanced)\n\nFor more control, you can use the DiscriminatorAwareBaseModel for your containers:\n\n```python\nfrom pydantic_discriminated import (\n discriminated_model, DiscriminatedBaseModel, \n DiscriminatorAwareBaseModel, DiscriminatedConfig\n)\n\n# Disable monkey patching\nDiscriminatedConfig.disable_monkey_patching()\n\n@discriminated_model(\"shape_type\", \"circle\")\nclass Circle(DiscriminatedBaseModel):\n radius: float\n\n# Use DiscriminatorAwareBaseModel for containers\nclass ShapeContainer(DiscriminatorAwareBaseModel):\n shape: Circle\n\ncontainer = ShapeContainer(shape=Circle(radius=5))\ndata = container.model_dump() # Will include shape_type\n```\n\n## Advanced Usage\n\n### Enum Discriminators\n\n```python\nfrom enum import Enum\nfrom pydantic_discriminated import discriminated_model, DiscriminatedBaseModel\n\nclass MessageType(str, Enum):\n TEXT = \"text\"\n IMAGE = \"image\"\n VIDEO = \"video\"\n\n@discriminated_model(MessageType, MessageType.TEXT)\nclass TextMessage(DiscriminatedBaseModel):\n content: str\n\n@discriminated_model(MessageType, MessageType.IMAGE)\nclass ImageMessage(DiscriminatedBaseModel):\n url: str\n width: int\n height: int\n```\n\n### Standard Fields\n\nBy default, discriminator fields are included both as domain-specific fields (e.g., `shape_type`) and as standard fields for interoperability:\n\n```python\ncircle = Circle(radius=5)\ndata = circle.model_dump()\n# Results in:\n# {\n# \"radius\": 5,\n# \"shape_type\": \"circle\", # Domain-specific discriminator\n# \"discriminator_category\": \"shape_type\", # Standard category field\n# \"discriminator_value\": \"circle\" # Standard value field\n# }\n```\n\nYou can control this behavior globally or per-model:\n\n```python\n# Global configuration\nDiscriminatedConfig.use_standard_fields = False\n\n# Per-model configuration using model_config\n@discriminated_model(\"animal_type\", \"cat\")\nclass Cat(DiscriminatedBaseModel):\n model_config = {\"use_standard_fields\": False}\n name: str\n lives_left: int\n\n# Direct parameter in decorator\n@discriminated_model(\"animal_type\", \"dog\", use_standard_fields=True)\nclass Dog(DiscriminatedBaseModel):\n name: str\n breed: str\n```\n\n## FastAPI Example\n\n```python\nfrom fastapi import FastAPI\nfrom typing import Union, List\n\napp = FastAPI()\n\n@app.post(\"/shapes/\")\ndef process_shape(shape: Union[Circle, Rectangle]):\n return {\"area\": shape.area()}\n\n@app.post(\"/shape-collection/\")\ndef process_shapes(shapes: ShapeCollection):\n return {\"total_area\": shapes.total_area()}\n```\n\nThis will automatically generate the correct OpenAPI schema with discriminator support!\n\n## License\n\nMIT\n\n---\n\nThis library fills a significant gap in Pydantic's functionality. If you work with polymorphic data structures, it will make your life easier!",
"bugtrack_url": null,
"license": "MIT",
"summary": "Type-safe discriminated unions for Pydantic models",
"version": "0.1.28",
"project_urls": {
"Bug Tracker": "https://github.com/talbotknighton/pydantic-discriminated/issues",
"Documentation": "https://talbotknighton.github.io/pydantic-discriminated/",
"Homepage": "https://github.com/talbotknighton/pydantic-discriminated",
"PyPI": "https://pypi.org/project/pydantic-discriminated/",
"Source Code": "https://github.com/talbotknighton/pydantic-discriminated"
},
"split_keywords": [
"discriminated",
" polymorphic",
" pydantic",
" serialization",
" unions",
" validation"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "b3b54f506cfd9ade1d581749a8348e65104b78d745fa5e85526281d810ffc79d",
"md5": "5d510678deeb5b989e8ce0ea4feae30c",
"sha256": "98774fbd6f5a5aaf6286b74e0f932a03b055e780681e79cf047c75fedeae0681"
},
"downloads": -1,
"filename": "pydantic_discriminated-0.1.28-py3-none-any.whl",
"has_sig": false,
"md5_digest": "5d510678deeb5b989e8ce0ea4feae30c",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 18801,
"upload_time": "2025-08-22T00:21:16",
"upload_time_iso_8601": "2025-08-22T00:21:16.418024Z",
"url": "https://files.pythonhosted.org/packages/b3/b5/4f506cfd9ade1d581749a8348e65104b78d745fa5e85526281d810ffc79d/pydantic_discriminated-0.1.28-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "714cecc69d4c28699260cb1875dca37207209c06a0a369f451393fd8ebaf167e",
"md5": "da4e3e765d80e4b82393619bfce3eb41",
"sha256": "665000bc14204de9e3f7f541611130f4a7202276b682bf3b6111c24ea31b7f5b"
},
"downloads": -1,
"filename": "pydantic_discriminated-0.1.28.tar.gz",
"has_sig": false,
"md5_digest": "da4e3e765d80e4b82393619bfce3eb41",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 19662,
"upload_time": "2025-08-22T00:21:17",
"upload_time_iso_8601": "2025-08-22T00:21:17.993360Z",
"url": "https://files.pythonhosted.org/packages/71/4c/ecc69d4c28699260cb1875dca37207209c06a0a369f451393fd8ebaf167e/pydantic_discriminated-0.1.28.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-08-22 00:21:17",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "talbotknighton",
"github_project": "pydantic-discriminated",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "pydantic-discriminated"
}