Max McCord
07/22/2025, 7:58 PMclass DateRange(BaseModel):
model_config = {'title': 'Range'}
mode: Literal['range'] = Field('range', exclude=True)
start_date: date = Field(title='Start Date (inclusive)')
end_date: date = Field(title='End Date (inclusive)')
class DateList(BaseModel):
model_config = {'title': 'List'}
mode: Literal['list'] = Field('list', exclude=True)
dates: list[date] = Field(default_factory=list, title='Dates')
# This is parsed properly in the UI, but shows me a const, uneditable "mode" field.
DateParam1 = DateList | DateRange
# This is not parsed properly, and forces the UI to require JSON input
DateParam2 = Annotated[DateList | DateRange, Field(discriminator='mode')]
Marvin
07/22/2025, 7:58 PMMarvin
07/22/2025, 7:59 PMAnnotated
with Tag
and Discriminator
to properly handle the Union type while controlling the discriminator field visibility. Here's how you can modify your code:
python
from typing import Annotated, Union, Any
from datetime import date
from pydantic import BaseModel, Field, Tag, Discriminator
from typing_extensions import Literal
def date_param_discriminator(v: Any) -> str:
if isinstance(v, dict):
return v.get("mode", "")
return getattr(v, "mode", "")
class DateRange(BaseModel):
model_config = {'title': 'Range'}
mode: Literal['range'] = Field('range', exclude=True)
start_date: date = Field(title='Start Date (inclusive)')
end_date: date = Field(title='End Date (inclusive)')
class DateList(BaseModel):
model_config = {'title': 'List'}
mode: Literal['list'] = Field('list', exclude=True)
dates: list[date] = Field(default_factory=list, title='Dates')
# This version will properly handle the discriminator while hiding the mode field
DateParam = Annotated[
Union[
Annotated[DateRange, Tag("range")],
Annotated[DateList, Tag("list")]
],
Discriminator(date_param_discriminator)
]
This solution:
1. Uses Annotated
with Tag
to mark each type in the Union with its corresponding mode
2. Uses a Discriminator
with a custom discriminator function to determine which type to use
3. Keeps the mode
field excluded from the UI while still using it for type discrimination
4. Maintains the proper titles and field descriptions in the UI
You can then use this in your flow like this:
python
from prefect import flow
@flow
def process_dates(date_param: DateParam):
if isinstance(date_param, DateRange):
print(f"Processing date range: {date_param.start_date} to {date_param.end_date}")
else:
print(f"Processing date list: {date_param.dates}")
The UI will now show the proper form fields without exposing the discriminator field, and the type discrimination will work correctly when the flow is called.
This approach is based on Pydantic's recommended way of handling discriminated unions and is compatible with Prefect 3.x's parameter handling. The discriminator function helps determine which type to use based on the mode
field, while keeping that field hidden from the UI.
Note: If you're experiencing any issues with Pydantic versions, make sure you're using a compatible version as there have been some recent compatibility issues with newer Pydantic releases (as evidenced in the GitHub issues).Max McCord
07/22/2025, 8:07 PMPydanticUserError("`Tag` not provided for choice {'type': 'tagged-union', 'choices': {'range': {'type': 'definition-ref', 'schema_ref': '...', 'metadata': {'pydantic.internal.tagged_union_tag': 'range'}}, 'list': {'type': 'definition-ref', 'schema_ref': '...', 'metadata': {'pydantic.internal.tagged_union_tag': 'list'}}}, 'discriminator': <function date_param_discriminator at 0xffff746e6680>} used with `Discriminator`")
Marvin
07/22/2025, 8:07 PMMarvin
07/22/2025, 8:10 PMpython
from typing import Annotated, Union, Literal
from datetime import date
from pydantic import BaseModel, Field, ConfigDict
class DateRange(BaseModel):
model_config = ConfigDict(title='Range')
type: Literal["range"] = Field("range")
start_date: date = Field(title='Start Date (inclusive)')
end_date: date = Field(title='End Date (inclusive)')
class DateList(BaseModel):
model_config = ConfigDict(title='List')
type: Literal["list"] = Field("list")
dates: list[date] = Field(default_factory=list, title='Dates')
# Use a simpler Union approach with a discriminator field
class DateParam(BaseModel):
model_config = ConfigDict(
json_schema_extra={
"discriminator": {"propertyName": "type"},
"oneOf": [
{"$ref": f"#/definitions/{DateRange.__name__}"},
{"$ref": f"#/definitions/{DateList.__name__}"},
],
}
)
__root__: Union[DateRange, DateList]
def __init__(self, **data):
super().__init__(__root__=data)
@property
def value(self) -> Union[DateRange, DateList]:
return self.__root__
You can then use it in your flow like this:
python
from prefect import flow
@flow
def process_dates(date_param: DateParam):
param = date_param.value
if isinstance(param, DateRange):
print(f"Processing date range: {param.start_date} to {param.end_date}")
else:
print(f"Processing date list: {param.dates}")
This approach:
1. Uses a more straightforward discriminator pattern with a type
field
2. Wraps the Union in a container model that properly handles the discriminator
3. Avoids the complexity of the Tag
and Discriminator
annotations that were causing issues
4. Still provides a clean interface in the UI with proper type discrimination
The UI should now show the proper form fields and handle the type discrimination correctly. The type
field will be visible but will be automatically set based on which form variant is selected.
Note: If you're still experiencing issues, you might want to check your Pydantic version. The recent Pydantic 2.11.1 release has caused some compatibility issues with Prefect. You might need to pin to a specific version of Pydantic that's known to work well with your version of Prefect.
Try this version and let me know if you encounter any other issues. We can further refine the solution based on your specific needs or any errors you encounter.Max McCord
07/22/2025, 8:17 PMNate
07/22/2025, 8:24 PMNate
07/22/2025, 8:24 PMLiteral
or Union
directly?Nate
07/22/2025, 8:24 PMMax McCord
07/22/2025, 10:24 PM