I am trying to use Literal to create a discriminated union with Pydantic.
There are events about a Job resource, and I want to distinguish them by event_name. For JobPublishedEvents I want to ensure, that some extra_field is present.
class GenericJobEvent(BaseModel):
event_name: str
id: int
class JobPublishedEvent(GenericJobEvent):
event_name: Literal['job.published']
extra_field: str
class Wrapper(BaseModel):
wrapped: Union[JobPublishedEvent, GenericJobEvent]
print(type(Wrapper(wrapped={'event_name': 'some.event', 'id': 1}).wrapped)) # GenericJobEvent
print(type(Wrapper(wrapped={'event_name': 'job.published', 'id': 1, 'extra_field': 'extra'}).wrapped)) # JobPublishedEvent
print(type(Wrapper(wrapped={'event_name': 'job.published', 'id': 1}).wrapped)) # GenericJobEvent
The first 2 cases behave as expected, for the third I would like a validation error since the literal matches, but the schema is not fulfilled. I get why the fallback to the GenericJobEvent is valid, though.
Does anybody have an idea on how to achieve this?
The problem is due to the fact that while JobPublishedEvent fails, GenericJobEvent does not. Think about it, the event_name attribute is a string, the id is an int. Everything matches.
So, what you could do, is to validate also the event_name in GenericJobEvent and reject the event name job.published
You can validate it as explained in the docs
@validator('event_name')
def event_name_filter(cls, v):
if 'job.published' == v:
raise ValueError('GenericJobEvent cannot have event_name equal to job.published')
return v.title()