I am trying to validate Slack interaction payloads, that look like these:
type: block_actions
container:
type: view
...
type: block_actions
container:
type: message
...
type: view_submission
...
I use 3 different models for payloads coming to the same interaction endpoint:
class MessageContainer(BaseModel):
type: Literal["message"]
...
class ViewContainer(BaseModel):
type: Literal["view"]
...
class MessageActions(ActionsBase):
type: Literal["block_actions"]
container: MessageContainer
...
class ViewActions(ActionsBase):
type: Literal["block_actions"]
container: ViewContainer
...
class ViewSubmission(BaseModel):
type: Literal["view_submission"]
...
and I was planning to use
BlockActions = Annotated[
MessageActions | ViewActions,
Field(discriminator="container.type"),
]
SlackInteraction = Annotated[
ViewSubmission | BlockActions,
Field(discriminator="type"),
]
SlackInteractionAdapter = TypeAdapter(SlackInteraction)
but cannot make it work with v2.10.4.
Do I have to dispatch them manually or there is a way to solve it with Pydantic?
Not sure it's possible to use 2 discriminators to resolve one type (as you are trying to do).
I can suggest you 3 options:
1. Split block_actions
into block_message_actions
and block_view_actions
:
from typing import Annotated, Literal
from pydantic import BaseModel, Field, TypeAdapter
class MessageContainer(BaseModel):
pass
class ViewContainer(BaseModel):
pass
class ActionsBase(BaseModel):
pass
class MessageActions(ActionsBase):
type: Literal["block_message_actions"]
container: MessageContainer
class ViewActions(ActionsBase):
type: Literal["block_view_actions"]
container: ViewContainer
class ViewSubmission(BaseModel):
type: Literal["view_submission"]
SlackInteraction = Annotated[
ViewSubmission | ViewActions | MessageActions,
Field(discriminator="type"),
]
SlackInteractionAdapter = TypeAdapter(SlackInteraction)
a = SlackInteractionAdapter.validate_python({"type": "view_submission"})
assert isinstance(a, ViewSubmission)
b = SlackInteractionAdapter.validate_python(
{"type": "block_message_actions", "container": {}},
)
assert isinstance(b, MessageActions)
assert isinstance(b.container, MessageContainer)
c = SlackInteractionAdapter.validate_python(
{"type": "block_view_actions", "container": {}},
)
assert isinstance(c, ViewActions)
assert isinstance(c.container, ViewContainer)
2. Use Discriminated Unions with callable Discriminator:
def get_discriminator_value(v: Any) -> str:
if isinstance(v, dict):
if v["type"] == "view_submission":
return "view_submission"
return "message_action" if v["container"]["type"] == "message" else "view_action"
if v.type == "view_submission":
return "view_submission"
return "message_action" if v.container.type == "message" else "view_action"
SlackInteraction = Annotated[
Union[
Annotated[ViewSubmission, Tag("view_submission")],
Annotated[MessageActions, Tag("message_action")],
Annotated[ViewActions, Tag("view_action")],
],
Discriminator(get_discriminator_value),
]
SlackInteractionAdapter = TypeAdapter(SlackInteraction)
3. Use nested discriminated unions:
from typing import Annotated, Literal
from pydantic import BaseModel, Field, TypeAdapter
class MessageContainer(BaseModel):
type: Literal["message"]
class ViewContainer(BaseModel):
type: Literal["view"]
ActionContainer = Annotated[
MessageContainer | ViewContainer,
Field(discriminator="type"),
]
class BlockActions(BaseModel):
type: Literal["block_actions"]
container: ActionContainer
class ViewSubmission(BaseModel):
type: Literal["view_submission"]
SlackInteraction = Annotated[
ViewSubmission | BlockActions,
Field(discriminator="type"),
]
SlackInteractionAdapter = TypeAdapter(SlackInteraction)
a = SlackInteractionAdapter.validate_python({"type": "view_submission"})
assert isinstance(a, ViewSubmission)
b = SlackInteractionAdapter.validate_python(
{"type": "block_actions", "container": {"type": "message"}},
)
assert isinstance(b, BlockActions)
assert isinstance(b.container, MessageContainer)
c = SlackInteractionAdapter.validate_python(
{"type": "block_actions", "container": {"type": "view"}},
)
assert isinstance(c, BlockActions)
assert isinstance(c.container, ViewContainer)