I want to make a reusable serializer I can pass arguments too. Currently I'm doing this:
class Example(BaseModel):
model_config = ConfigDict(validate_default=True)
test_model_list_1: Annotated[
list[ExampleNestedOne],
PlainSerializer(exclude_fields(fields=["bbb", "ccc"])),
]
test_model_list_2: Annotated[
list[ExampleNestedOne],
PlainSerializer(exclude_fields(fields=["aaa", "bbb"])),
]
But I want to do something like this, is this possible somehow?
model_list_serializer = Annotated[
list[ExampleNestedOne],
PlainSerializer(exclude_fields()),
]
class Example(BaseModel):
model_config = ConfigDict(validate_default=True)
test_model_list_1: model_list_serializer # pass in ["bbb", "ccc"]
test_model_list_2: model_list_serializer # pass in ["aaa", "bbb"]
Here is the entire code snippet:
def exclude_fields(
fields: list[str] | None = None,
) -> Callable[[list[BaseModel]], list[dict[str, Any]]]:
"""Exclude fields from a list of nested models."""
def _exclude_nested(
model_list: list[BaseModel],
) -> list[dict[str, Any]]:
"""This is for serialization, so it should return a list of dicts."""
return [model.model_dump(exclude=fields) for model in model_list]
return _exclude_nested
def model_list_serializer(model_list: list[BaseModel], fields: list[str]):
return Annotated[
list[ExampleNestedOne],
PlainSerializer(exclude_fields(fields=["bbb", "ccc"])),
]
class ExampleNestedOne(BaseModel):
aaa: str = "aaaaaa"
bbb: str = "bbbbbb"
ccc: str = "cccccc"
class Example(BaseModel):
model_config = ConfigDict(validate_default=True)
test_model_list_1: Annotated[
list[ExampleNestedOne],
PlainSerializer(exclude_fields(fields=["bbb", "ccc"])),
]
test_model_list_2: Annotated[
list[ExampleNestedOne],
PlainSerializer(exclude_fields(fields=["aaa", "bbb"])),
]
def test_derps():
instance = Example(
test_model_list_1=[ExampleNestedOne(), ExampleNestedOne()],
test_model_list_2=[ExampleNestedOne(), ExampleNestedOne()],
)
You cannot pass arguments into a plain Annotated alias because it becomes a fixed type the moment you assign it. The trick is to use a small factory that returns a new Annotated type each time. That lets you parameterize the serializer exactly the way you want.
Here is the clean working pattern.
from typing import Annotated, Any
from pydantic import BaseModel, ConfigDict, PlainSerializer
def exclude_fields(fields: list[str] | None = None):
"""Exclude fields from a list of nested models."""
def _exclude_nested(model_list: list[BaseModel]):
return [model.model_dump(exclude=fields) for model in model_list]
return _exclude_nested
class ExampleNestedOne(BaseModel):
aaa: str = "aaaaaa"
bbb: str = "bbbbbb"
ccc: str = "cccccc"
def model_list_serializer(fields: list[str]):
"""Factory that produces an Annotated serializer with the requested exclusions."""
return Annotated[
list[ExampleNestedOne],
PlainSerializer(exclude_fields(fields=fields)),
]
class Example(BaseModel):
model_config = ConfigDict(validate_default=True)
test_model_list_1: model_list_serializer(["bbb", "ccc"])
test_model_list_2: model_list_serializer(["aaa", "bbb"])
def test_derps():
instance = Example(
test_model_list_1=[ExampleNestedOne(), ExampleNestedOne()],
test_model_list_2=[ExampleNestedOne(), ExampleNestedOne()],
)
print(instance.model_dump())
This works because Annotated is evaluated at class creation time. Calling a function there is perfectly valid and Pydantic will receive a fresh PlainSerializer instance with your custom field list.