pydantic-discriminated


Namepydantic-discriminated JSON
Version 0.1.28 PyPI version JSON
download
home_pageNone
SummaryType-safe discriminated unions for Pydantic models
upload_time2025-08-22 00:21:17
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseMIT
keywords discriminated polymorphic pydantic serialization unions validation
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # pydantic-discriminated

A robust, type-safe implementation of discriminated unions for Pydantic models.

[![PyPI version](https://img.shields.io/pypi/v/pydantic-discriminated.svg)](https://pypi.org/project/pydantic-discriminated/)
[![Python versions](https://img.shields.io/pypi/pyversions/pydantic-discriminated.svg)](https://pypi.org/project/pydantic-discriminated/)
[![License](https://img.shields.io/github/license/TalbotKnighton/pydantic-discriminated.svg)](https://github.com/TalbotKnighton/pydantic-discriminated/blob/main/LICENSE)
[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://talbotknighton.github.io/pydantic-discriminated/)
[![GitHub](https://img.shields.io/github/stars/TalbotKnighton/pydantic-discriminated?style=social)](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[![PyPI version](https://img.shields.io/pypi/v/pydantic-discriminated.svg)](https://pypi.org/project/pydantic-discriminated/)\n[![Python versions](https://img.shields.io/pypi/pyversions/pydantic-discriminated.svg)](https://pypi.org/project/pydantic-discriminated/)\n[![License](https://img.shields.io/github/license/TalbotKnighton/pydantic-discriminated.svg)](https://github.com/TalbotKnighton/pydantic-discriminated/blob/main/LICENSE)\n[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://talbotknighton.github.io/pydantic-discriminated/)\n[![GitHub](https://img.shields.io/github/stars/TalbotKnighton/pydantic-discriminated?style=social)](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"
}
        
Elapsed time: 0.51882s