✨ Support providing serializing context to response models#11634
✨ Support providing serializing context to response models#11634alexcouper wants to merge 17 commits intofastapi:masterfrom
Conversation
a3ea200 to
6f09547
Compare
|
Hi @alexcouper 👋, I'm wondering if your proposal can be used to handle my use-case: I'd like a parameter passed to an endpoint to expand some datamodels. By default I want one property being serialized with The specifics here compared to your example is the fact the context should be hinted by the call itself. |
|
If you want the behaviour to be determined by a query param flag then I'm not sure this will help you -at least you won't be able to make use of the One way round this would be to have 2 route endpoints dependent on expansion that use the same underlying function to get the objects. If you do that you can use the |
Yeah, but the "2 route endpoint" is not something I'd consider. Basically I want the behavior to be driven by query, such as I guess it's worth logging a new issue at that point. |
66d13a4 to
25716e3
Compare
|
Hi @alexcouper, thanks for the PR! We'll get this reviewed by the team and get back to you 🙏 |
404a85f to
90eb570
Compare
| exclude_unset=exclude_unset, | ||
| exclude_defaults=exclude_defaults, | ||
| exclude_none=exclude_none, | ||
| ) |
There was a problem hiding this comment.
There were linting issues when changing this to use dict approach from #11670 so leaving as is.
|
@svlandeg should I be regularly rebasing this PR, as conflicts come in, or is it OK to wait until a first review is done? |
|
Any update on this? This would be a useful feature and am keen to help land. |
There was a problem hiding this comment.
@alexcouper, thanks for working on this!
First we need to decide whether we want to implement this feature this way or we also want to support passing values from request.
Let's wait for Sebastián to look at this and take a decision
UPD:
Found the problem in this implementation.
Imagine we want to remove some field if we serialize model not in FastAPI context:
class MultiUseItem(BaseModel):
name: str
owner_ids: Optional[list[int]] = None
@model_serializer(mode="wrap")
def _serialize(self, handler, info: SerializationInfo):
data = handler(self)
if info.context and info.context.get("mode") == "FASTAPI":
pass
else: # context is not passed or mode is not FASTAPI
if "owner_ids" in data:
data.pop("owner_ids")
return dataIf we try to use this model as response_model and return the instance of it from the endpoint, it will be serialized by jsonable_encoder without context passed and this way we loose owner_ids.
Full code example in details
Details
from typing import Optional
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, SerializationInfo, model_serializer
class MultiUseItem(BaseModel):
name: str
owner_ids: Optional[list[int]] = None
@model_serializer(mode="wrap")
def _serialize(self, handler, info: SerializationInfo):
data = handler(self)
if info.context and info.context.get("mode") == "FASTAPI":
pass
else: # context is not passed or mode is not FASTAPI
if "owner_ids" in data:
data.pop("owner_ids")
return data
app = FastAPI()
@app.get(
"/items/exclude-if-not-fastapi",
response_model=dict[str, MultiUseItem],
response_model_context={"mode": "FASTAPI"},
)
async def get_validdict_with_context():
return {"k": MultiUseItem(name="baz", owner_ids=[1, 2, 3])}
client = TestClient(app)
def test_exclude_if_not_fastapi_context():
response = client.get("/items/exclude-if-not-fastapi")
response.raise_for_status()
expected_response = {"k": {"name": "baz", "owner_ids": [1, 2, 3]}}
assert response.json() == expected_responseSo, we also need to pass serialization context to jsonable_encoder.
This PR #13475 modifies jsonable_encoder to accept the context
| - "3.9" | ||
| - "3.8" | ||
| pydantic-version: ["pydantic-v1", "pydantic-v2"] | ||
| pydantic-version: ["pydantic-v1", "pydantic-v2.7", "pydantic-v2.8+"] |
There was a problem hiding this comment.
I think we don't need this.
There are other places where the code may fail with older versions of Pydantic and if we try to cover all such cases we will end up having huge amount of runs
| # | ||
| # context argument was introduced in pydantic 2.8 | ||
| if PYDANTIC_VERSION >= "2.8": | ||
| return self._type_adapter.dump_python( | ||
| value, | ||
| mode=mode, | ||
| include=include, | ||
| exclude=exclude, | ||
| by_alias=by_alias, | ||
| exclude_unset=exclude_unset, | ||
| exclude_defaults=exclude_defaults, | ||
| exclude_none=exclude_none, | ||
| context=context, | ||
| ) | ||
| else: | ||
| return self._type_adapter.dump_python( | ||
| value, | ||
| mode=mode, | ||
| include=include, | ||
| exclude=exclude, | ||
| by_alias=by_alias, | ||
| exclude_unset=exclude_unset, | ||
| exclude_defaults=exclude_defaults, | ||
| exclude_none=exclude_none, | ||
| ) |
There was a problem hiding this comment.
| # | |
| # context argument was introduced in pydantic 2.8 | |
| if PYDANTIC_VERSION >= "2.8": | |
| return self._type_adapter.dump_python( | |
| value, | |
| mode=mode, | |
| include=include, | |
| exclude=exclude, | |
| by_alias=by_alias, | |
| exclude_unset=exclude_unset, | |
| exclude_defaults=exclude_defaults, | |
| exclude_none=exclude_none, | |
| context=context, | |
| ) | |
| else: | |
| return self._type_adapter.dump_python( | |
| value, | |
| mode=mode, | |
| include=include, | |
| exclude=exclude, | |
| by_alias=by_alias, | |
| exclude_unset=exclude_unset, | |
| exclude_defaults=exclude_defaults, | |
| exclude_none=exclude_none, | |
| ) | |
| kwargs: dict[str, Any] = ( | |
| {"context": context} if PYDANTIC_VERSION >= "2.8" else {} | |
| ) | |
| return self._type_adapter.dump_python( | |
| value, | |
| mode=mode, | |
| include=include, | |
| exclude=exclude, | |
| by_alias=by_alias, | |
| exclude_unset=exclude_unset, | |
| exclude_defaults=exclude_defaults, | |
| exclude_none=exclude_none, | |
| **kwargs, | |
| ) |
This doesn't give any linting issues
|
|
||
| This will be passed in as serialization context to the response model. | ||
|
|
||
| Note: This feature is a noop on pydantic < 2.8 |
There was a problem hiding this comment.
| Note: This feature is a noop on pydantic < 2.8 | |
| Note: This feature requires Pydantic 2.8 or higher |
|
This pull request has a merge conflict that needs to be resolved. |
Context
pydantic/pydantic#9495 introduces passing context to
TypeAdaptermodels.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
excludekeyword (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
And this model definition can be made more generalized like so:
TODO: