Replies: 1 comment 8 replies
-
My team had the same requirement as #3091, and a solution inspired by the approved answer worked until we upgraded to V2. The inner workings of the new pydantic are somewhat alien to me still, but we managed to get it to work using the documentation for custom data types. Basically, we can use from typing import Any, Callable, List
from pydantic import BaseModel as BaseModel
from pydantic_core import core_schema
class BaseModel(BaseModel, extra="forbid"):
__polymorphic__ = False
@classmethod
def __get_pydantic_core_schema__(
cls, __source: type["BaseModel"], __handler: Callable[[Any], core_schema.CoreSchema]
) -> core_schema.CoreSchema:
schema = __handler(__source)
og_schema_ref = schema["ref"]
schema["ref"] += ":aux"
return core_schema.no_info_before_validator_function(
cls.__convert_to_real_type__, schema=schema, ref=og_schema_ref
)
@classmethod
def __convert_to_real_type__(cls, value: Any):
if not cls.__polymorphic__:
return value
if isinstance(value, dict) is False:
return value
value = value.copy()
subclass = value.pop("kind", None)
if subclass is None:
raise ValueError(f"Missing 'kind' in {cls.__name__}")
try:
sub = next(dtype for dtype in cls.__subclasses__() if dtype.__name__.lower() == subclass)
except StopIteration as e:
raise TypeError(f"Unsupported subclass: {subclass}") from e
return sub(**value)
def __init_subclass__(cls, polymorphic: bool = False, **kwargs):
cls.__polymorphic__ = polymorphic
super().__init_subclass__(**kwargs)
class Pet(BaseModel, polymorphic=True):
name: str
class Cat(Pet):
meows: int
class Dog(Pet):
barks: float
friend: Pet
class Bird(Pet):
species: str
class Bee(Pet):
species: str
class Person(BaseModel):
name: str
pet1: List[Pet] = []
pet2: List[Pet] = []
pets = []
pets.append({"name": "Tiger", "kind": "cat", "meows": 42})
pets.append({"name": "Fluffy", "kind": "dog", "barks": 3.14, "friend": {"kind": "cat", "meows": 10, "name": "Tiger"}})
pets.append({"name": "Ace", "kind": "bird", "species": "eagle"})
pets.append({"name": "Bee", "kind": "bee", "species": "western honey bee"})
for pet in pets:
p = Person(**{"name": f"Alice with a {pet['kind']}", "pet1": [pet], "pet2": [pet]})
print(p)
The only weird part I don't fully understand why it would be necessary is the following: ...
og_schema_ref = schema["ref"]
schema["ref"] += ":aux"
... If this is not included, whenever Similar results can definitely also be achieved using |
Beta Was this translation helpful? Give feedback.
-
I've been using Pydantic to validate third-party REST API's for a while, and it does a fantastic job keeping the code clean. As I'm moving to V2, there is one area I've struggled to find a suitable replacement: polymorphic subclasses.
I initially implemented the suggestion in #3091 (specifically overriding
parse_obj
), however that approach does not work formodel_validate
in V2 (assumedly due to the way it is implemented in pydantic_core). I've also tried following along with suggestions in #5785, howeverTypeAdapter
and discriminated unions are not quite enough for my use case.Given the following example:
As expected, the types for
TIGER
andFLUFFY
are parsed asPet
objects when validated asPerson.pet
members. Is there a supported method for resolvingPerson.pet
to the correct subclass based on type?I've tried adding a
model_validator
toAnimal
, but that just causes infinite recursion:It would be possible to list all the valid types rather than the parent class:
But this would require a significant refactoring of the codebase and opens up many opportunities for errors (namely when I forget to update lists in each class). I appreciated the clarity of the fully-defined schema when listing out these members, however for this application that would be less than desirable.
It may be "that's just how things work" which is a fine answer... I'm just looking for some direction before I have to find a more "invasive" option.
Beta Was this translation helpful? Give feedback.
All reactions