Yes, in fact, this extends to quite a few standard library types already. This is what I want to extend to arbitrary types that don’t need explicit support from framework authors.
There’s also the fact that a third-party type could be using a vanilla class, pydantic.BaseModel
, or msgspec.Struct
completely out of your control. This third-party type may most likely also provide methods that have already done the hard work of (de)serialization, but neither Pydantic nor Msgspec would know how to invoke this.
Take this example. I wrote a simple Package
class where I want to use third-party types like packaging.version.Version
and whenever.Instant
.
from typing import Annotated, Any
import msgspec
from packaging.version import InvalidVersion, Version
from pydantic import BaseModel, BeforeValidator, ConfigDict, PlainSerializer
from whenever import Instant
# --- Pydantic Implementation ---
def version_validator(obj: Any) -> Version:
if isinstance(obj, str):
try:
return Version(obj)
except InvalidVersion:
raise ValueError("Bad Version!")
raise ValueError(f"Cannot convert {type(obj).__name__} to Version.")
class PackageModel(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
name: str
version: Annotated[Version, BeforeValidator(version_validator), PlainSerializer(lambda ver: str(ver), return_type=str, when_used='json')]
upload_date: Instant # whenever.Instant supports Pydantic out of the box.
# --- Msgspec Implementation ---
def enc_hook(obj: Any) -> Any:
if isinstance(obj, Version):
return str(obj)
if isinstance(obj, Instant):
return obj.format_common_iso()
raise NotImplementedError(f"Objects of type {type(obj)} are not supported")
def dec_hook(type: type[Any], obj: Any) -> Any:
if type is Version and isinstance(obj, str):
return Version(obj)
if type is Instant and isinstance(obj, str):
return Instant.parse_common_iso(obj)
raise NotImplementedError(f"Objects of type {type} are not supported")
class PackageStruct(msgspec.Struct):
name: str
version: Version
upload_date: Instant
# --- Usage Example ---
obj = {"name": "foobar","version": "1.0", "upload_date": "2025-07-18 16:12:30.481346+00:00"}
pydantic_test = PackageModel.model_validate(obj)
msgspec_test = msgspec.convert(obj, type=PackageStruct, dec_hook=dec_hook)
assert pydantic_test.upload_date == msgspec_test.upload_date
assert pydantic_test.version == msgspec_test.version
assert pydantic_test.model_dump_json() == msgspec.json.encode(msgspec_test, enc_hook=enc_hook).decode()
As you can see, even though types like packaging.version.Version
and whenever.Instant
inherently know how to handle their own (de)serialization, I still had to write specific validators and hooks for Pydantic and Msgspec, respectively. This boilerplate is exactly what I’m hoping to avoid for arbitrary types that already provide their own constructor methods.
You’ll also notice how whenever.Instant
happens to implement Pydantic support (which is identical to calling Instant.parse_common_iso(obj)
). This is great if you’re using Pydantic, otherwise you’re out of luck.
A Note on Third-Party Libraries:
I’m not in any way suggesting this is a fault on the authors of any third-party types mentioned in this post. They’ve done an amazing job already, and it’s up to them which framework to support (if any). It’s also not practical to ask every library author to implement support for every popular framework.