-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Discriminated Unions with callable Discriminator
fail to create a class dynamically with Optional
annotation
#9321
Comments
Thanks for the detailed description and reproducible code snippet. Definitely looks like a bug. Can be a bit difficult to track down with our complicated discriminator application logic, but I'll look into a fix for this in 2.8! |
If anyone wants to take a stab in the meantime, be my guest :). |
Hi! I was able to look into this further - I think it's actually just a syntax fix when specifying the default value for the fields in the from typing import Any, Literal, Optional, Union
from pydantic import BaseModel, Discriminator, Field, Tag, create_model
from typing_extensions import Annotated
# Definitions
def model_x_discriminator(v: Any) -> str:
if isinstance(v, int):
return "int"
if isinstance(v, str):
return "str"
raise ValueError()
class ValueInt(BaseModel):
type: Literal["int"] = "int"
x: int
class ValueStr(BaseModel):
type: Literal["str"] = "str"
x: str
# old-style discriminator by class property
OldDiscriminatedType = Annotated[
Union[ValueInt, ValueStr],
Field(discriminator="type"),
]
class OldDiscriminatedModel(BaseModel):
value: OldDiscriminatedType
class OldDiscriminatedModelOptional(BaseModel):
value: Optional[OldDiscriminatedType]
print(
create_model(
"OldDiscriminatedModelOptionalTest",
__base__=OldDiscriminatedModel,
value=(Optional[OldDiscriminatedType], ...),
)
)
# outputs a new class
# new-style Discriminator and Tag
NewDiscriminatedType = Annotated[
Union[
Annotated[ValueInt, Tag("int")],
Annotated[ValueStr, Tag("str")],
],
Discriminator(model_x_discriminator),
]
class NewDiscriminatedModel(BaseModel):
value: NewDiscriminatedType
class NewDiscriminatedModelOptional(BaseModel):
value: Optional[NewDiscriminatedType]
new_value_field_info = NewDiscriminatedModel.model_fields["value"]
print(
create_model(
"NewDiscriminatedModelOptionalTest",
__base__=NewDiscriminatedModel,
value=(Optional[NewDiscriminatedType], ...),
)
)
# returns a class The |
Hi @sydney-runkle! Thank you for the response! I was using existing field info in order to preserve original setup (say, it's configured as from typing import Any, Literal, Optional, Union
from pydantic import BaseModel, Discriminator, Field, Tag, create_model
from pydantic.fields import FieldInfo
from typing_extensions import Annotated
def model_x_discriminator(v: Any) -> str:
if isinstance(v, int):
return "int"
if isinstance(v, str):
return "str"
raise ValueError()
class ValueInt(BaseModel):
type: Literal["int"] = "int"
x: int
class ValueStr(BaseModel):
type: Literal["str"] = "str"
x: str
NewDiscriminatedType = Annotated[
Union[
Annotated[ValueInt, Tag("int")],
Annotated[ValueStr, Tag("str")],
],
Discriminator(model_x_discriminator),
]
class NewDiscriminatedModel(BaseModel):
value: NewDiscriminatedType
class NewDiscriminatedModelOptional(BaseModel):
value: Optional[NewDiscriminatedType] = Field(default=None)
OldDiscriminatedType = Annotated[
Union[ValueInt, ValueStr],
Field(discriminator="type"),
]
class OldDiscriminatedModel(BaseModel):
value: OldDiscriminatedType
class OldDiscriminatedModelOptional(BaseModel):
value: Optional[OldDiscriminatedType] = Field(default=None)
new_value_field_info = NewDiscriminatedModel.model_fields["value"]
old_value_field_info = OldDiscriminatedModel.model_fields["value"]
print(
create_model(
"OldDiscriminatedModelOptionalTest",
__base__=OldDiscriminatedModel,
**{
"value": (
Optional[OldDiscriminatedType],
FieldInfo.merge_field_infos(old_value_field_info, Field(default=None)),
)
},
)
)
print(
create_model(
"NewDiscriminatedModelOptionalTest",
__base__=NewDiscriminatedModel,
**{
"value": (
Optional[NewDiscriminatedType],
FieldInfo.merge_field_infos(new_value_field_info, Field(default=None)),
)
},
)
) All works well for the old discriminator, however, creating a type based on new discriminator throws the same error. Could you confirm if this is a bug or still a syntax issue again? Thank you. |
Hi @chiselko6, Good question. I believe that error message is a reflection of the fact that you're using a field info new_value_field_info = NewDiscriminatedModelOptional.model_fields["value"]
NewDiscriminatedModelOptionalTest = create_model(
"NewDiscriminatedModelOptionalTest",
__base__=NewDiscriminatedModel,
value=(Optional[NewDiscriminatedType], FieldInfo.merge_field_infos(new_value_field_info, Field(default=ValueInt(x=1))))
)
print(NewDiscriminatedModelOptionalTest)
print(repr(NewDiscriminatedModelOptionalTest()))
#> NewDiscriminatedModelOptionalTest(value=ValueInt(type='int', x=1) In general, I think I'd advise against reusing field infos like this, but if you have to, I'd recommend keeping the type annotations the same when you reuse! You can also use annotated types for constraint reuse! |
Initial Checks
Description
In our code we need to convert existing pydantic
BaseModel
s into new classes with optional fields dynamically. We utilizepydantic.create_model
for that wrapping field annotations withOptional
. This works for fine for fields being discriminated unions defined asField(discriminator="<property>")
, but fails for ones defined asDiscriminator(callable)
with correspondingTag
annotation.In the provided example I built new-style and old-style discriminated unions side-by-side for comparison. The error the new-style discriminated union raises is the following, as if
NoneType
messes up with the inner union:Example Code
Python, Pydantic & OS Version
The text was updated successfully, but these errors were encountered: