pythonpython-typingpydanticpydantic-v2

How can I use a ClassVar Literal as a discriminator for Pydantic fields?


I'd like to have Pydantic fields that are discriminated based on a class variable.

For example:

from pydantic import BaseModel, Field
from typing import Literal, ClassVar

class Cat(BaseModel):
    animal_type: ClassVar[Literal['cat']] = 'cat'

class Dog(BaseModel):
    animal_type: ClassVar[Literal['dog']] = 'dog'

class PetCarrier(BaseModel):
    contains: Cat | Dog = Field(discriminator='animal_type')

But this code throws an exception at import time:

pydantic.errors.PydanticUserError: Model 'Cat' needs a discriminator field for key 'animal_type'

If I remove the ClassVar annotations, it works fine, but then animal_type is only available as an instance property, which is less convenient in my case.

Can anyone help me use class attributes as discriminators with Pydantic? This is Pydantic version 2.


Solution

  • This is a very interesting question. Broadly it seems to me that Pydantic does not support discriminated unions for ClassVar literals, but that asks the question, why not?

    An important thing to note about ClassVar's, is they don't become regular fields in pydantic (https://docs.pydantic.dev/2.3/usage/models/#class-vars). In some way they're quite a lot like PrivateAttr. They're only accessible via get, they don't feature in models. If we consider your question again but using PrivateAttr, it makes sense why this isn't supported:

    from pydantic import BaseModel, Field, PrivateAttr
    from typing import Literal
    
    class Cat(BaseModel):
        _animal_type: Literal['cat'] = PrivateAttr(default='cat')
    
    class Dog(BaseModel):
        _animal_type: Literal['dog'] = PrivateAttr(default='dog')
    
    class PetCarrier(BaseModel):
        contains: Cat | Dog = Field(discriminator='_animal_type')
    

    Field cannot access the discriminator _animal_type because it isn't accessible to it - it's a PrivateAttr. A discriminator has to be something that is a part of the model - even if instances aren't part of your use case, you have to consider what would happen if someone did make an instance.

    Just like PrivateAttr, ClassVar is a field that's not really part of the model, so it's impossible to use as a discriminator. In Pydantic I would think of ClassVar as kind of like a PrivateAttr that's publically accessible and can't be altered.