Following Up on Pydantic & Polymorphism
In my previous blog post, I discussed polymorphism with Pydantic and the issues it caused. I suggested an elegant solution (at least I think so), but one that was perhaps not the most plug and play option.
That said, we can try to simplify things and rely solely on Pydantic’s features to achieve the same goal, but this will require a few compromises. In particular, we will need to leverage Pydantic’s core schema generation API and use a specific annotation. It’s not ideal but there is not much choice, unless we switch back to the previous solution.
Solution
This post is going to be fairly light, I will not dive into the details. I will reuse my class hierarchy described in the previous post (as long as the Python setup), with a small addition to make sure the whole thing works across multiple levels.
| |
We draw inspiration from what is done for SerializeAsAny to hack together a schema on the fly for the declared classes. The trick here lies in identifying subclasses and generating a union schema dynamically, so we preserve correct serialization and deserialization while respecting polymorphism, and avoid doing anything too ugly with Python’s typing system.
type AnyOf[T] = ... syntax in the TYPE_CHECKING block requires Python 3.12+. For Python 3.10-3.11, you can use TypeAlias from typing instead. | |
The usage is then fairly straightforward. You just need to remember using the annotation when needed…
| |
Serialization and deserialization work as expected. Pydantic manages to find its way.
| |
{
"owner": {
"name": "Alice"
},
"animals": [
{
"name": "Buddy",
"age": 3,
"breed": "Golden Retriever"
},
{
"name": "Whiskers",
"age": 2,
"color": "Tabby"
},
{
"name": "Star",
"age": 5,
"height": 15.2,
"owner_name": "Alice"
}
]
}Conclusion
That’s it. Not much more to say. Just a small hack to get you unstuck. One of the main drawbacks of this lighter approach is the need to have all classes properly imported, otherwise Python will not be able to discover them dynamically and the schema generated by Pydantic may end up incomplete. In my opinion, the biggest annoyance is still the requirement to add a specific annotation whenever you want to support this use case. I would still tend to favor the previous solution in a well established codebase. In both cases, the solutions presented are sensitive to changes in Pydantic’s API, but unfortunately there isn’t much choice.
Anyway, just a quick little digression. See you.