fastapi icon indicating copy to clipboard operation
fastapi copied to clipboard

Support providing serializing context to response models

Open alexcouper opened this issue 1 month ago • 3 comments

Context

https://github.com/pydantic/pydantic/pull/9495 introduces passing context to TypeAdapter models.

Problem

Some fastapi based projects use model definitions that have multiple uses, and serializing for an API response is only one. Up until now, fastapi/pydantic has supported exclusion of fields through the use of exclude keyword (example from sentry).

If however, a model needs to be serialized in different contexts - for example to be saved to dynamodb as well as to return via the API - it becomes limiting to have to opt in for inclusion/exclusion once and for all.

Solution

Pydantic Serialization Contexts provide a means to tell pydantic models the context of the serialization, and then they can act on that.

This PR makes it possible to reuse model definitions within a fastapi project for multiple purposes, and then simply state during route definition the context you want to be included when rendering.

Example

Simple example


class SerializedContext(StrEnum):
    DYNAMODB = "dynamodb"
    FASTAPI = "fastapi"

class Item(BaseModel):
    name: str = Field(alias="aliased_name")
    price: Optional[float] = None
    owner_ids: Optional[List[int]] = None

    @model_serializer(mode="wrap")
    def _serialize(self, handler, info: SerializationInfo | None = None):
        data = handler(self)
        if info.context and info.context.get("mode") == SerializedContext.FASTAPI:
            if "price" in data:
                data.pop("price")
        return data


@app.get(
    "/items/validdict-with-context",
    response_model=Dict[str, Item],
    response_model_context={"mode": SerializedContext.FASTAPI},
)
async def get_validdict_with_context():

    return {
        "k1": Item(aliased_name="foo"),
        "k2": Item(aliased_name="bar", price=1.0),
        "k3": Item(aliased_name="baz", price=2.0, owner_ids=[1, 2, 3]),
    }

And this model definition can be made more generalized like so:

class ContextSerializable(BaseModel):
    @model_serializer(mode="wrap")
    def _serialize(self, handler, info: SerializationInfo):
        d = handler(self)

        serialize_context = info.context.get("mode")
        if not serialize_context:
            return d
        for k, v in self.model_fields.items():
            contexts = (
                v.json_schema_extra.get("serialized_contexts", [])
                if v.json_schema_extra
                else []
            )
            if contexts and serialize_context not in contexts:
                d.pop(k)

        return d

class Item(ContextSerializable):
    name: str = Field(alias="aliased_name")
    price: Optional[float] = Field(serialized_contexts={SerializedContext.DYNAMODB})
    owner_ids: Optional[List[int]] = None

TODO:

  • [x] Wait for https://github.com/pydantic/pydantic/pull/9495 to be merged
  • [ ] Wait for https://github.com/pydantic/pydantic/pull/9495 to be released (pydantic 2.8)
  • [ ] Update version specification in this PR

alexcouper avatar May 24 '24 00:05 alexcouper