Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to pass context to serialization (fix #7143) #8965

Merged
merged 9 commits into from Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/concepts/serialization.md
Expand Up @@ -702,6 +702,40 @@ print(person.model_dump(exclude_defaults=True)) # (3)!
2. `age` excluded from the output because `exclude_unset` was set to `True`, and `age` was not set in the Person constructor.
3. `age` excluded from the output because `exclude_defaults` was set to `True`, and `age` takes the default value of `None`.

## Serialization Context

You can pass a context object to the serialization methods which can be accessed from the `info`
ornariece marked this conversation as resolved.
Show resolved Hide resolved
argument to decorated serializer functions. This is useful when you need to dynamically update the
serialization behavior during runtime. For example, if you wanted a field to be dumped depending on
a dynamically controllable set of allowed values, this could be done by passing the allowed values
by context:

```python
from pydantic import BaseModel, SerializationInfo, field_serializer


class Model(BaseModel):
text: str

@field_serializer('text')
def remove_stopwords(self, v: str, info: SerializationInfo):
context = info.context
if context:
stopwords = context.get('stopwords', set())
v = ' '.join(w for w in v.split() if w.lower() not in stopwords)
return v


model = Model.model_construct(**{'text': 'This is an example document'})
print(model.model_dump()) # no context
#> {'text': 'This is an example document'}
print(model.model_dump(context={'stopwords': ['this', 'is', 'an']}))
#> {'text': 'example document'}
print(model.model_dump(context={'stopwords': ['document']}))
#> {'text': 'This is an example'}
```


ornariece marked this conversation as resolved.
Show resolved Hide resolved

## `model_copy(...)`

Expand Down
6 changes: 6 additions & 0 deletions pydantic/main.py
Expand Up @@ -292,6 +292,7 @@ def model_dump(
mode: typing_extensions.Literal['json', 'python'] | str = 'python',
include: IncEx = None,
exclude: IncEx = None,
context: dict[str, Any] | None = None,
by_alias: bool = False,
exclude_unset: bool = False,
exclude_defaults: bool = False,
Expand All @@ -309,6 +310,7 @@ def model_dump(
If mode is 'python', the output may contain non-JSON-serializable Python objects.
include: A set of fields to include in the output.
exclude: A set of fields to exclude from the output.
context: Additional context to pass to the serializer.
by_alias: Whether to use the field's alias in the dictionary key if defined.
exclude_unset: Whether to exclude fields that have not been explicitly set.
exclude_defaults: Whether to exclude fields that are set to their default value.
Expand All @@ -325,6 +327,7 @@ def model_dump(
by_alias=by_alias,
include=include,
exclude=exclude,
context=context,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
Expand All @@ -338,6 +341,7 @@ def model_dump_json(
indent: int | None = None,
include: IncEx = None,
exclude: IncEx = None,
context: dict[str, Any] | None = None,
by_alias: bool = False,
exclude_unset: bool = False,
exclude_defaults: bool = False,
Expand All @@ -353,6 +357,7 @@ def model_dump_json(
indent: Indentation to use in the JSON output. If None is passed, the output will be compact.
include: Field(s) to include in the JSON output.
exclude: Field(s) to exclude from the JSON output.
context: Additional context to pass to the serializer.
by_alias: Whether to serialize using field aliases.
exclude_unset: Whether to exclude fields that have not been explicitly set.
exclude_defaults: Whether to exclude fields that are set to their default value.
Expand All @@ -368,6 +373,7 @@ def model_dump_json(
indent=indent,
include=include,
exclude=exclude,
context=context,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
Expand Down
36 changes: 36 additions & 0 deletions tests/test_serialize.py
Expand Up @@ -1159,3 +1159,39 @@ class Foo(BaseModel):

foo_recursive = Foo(items=[Foo(items=[Baz(bar_id=42, baz_id=99)])])
assert foo_recursive.model_dump() == {'items': [{'items': [{'bar_id': 42}]}]}


def test_serialize_python_context() -> None:
contexts: List[Any] = [None, None, {'foo': 'bar'}]

class Model(BaseModel):
x: int

@field_serializer('x')
def serialize_x(self, v: int, info: SerializationInfo) -> int:
assert info.context == contexts.pop(0)
return v

m = Model.model_construct(**{'x': 1})
m.model_dump()
m.model_dump(context=None)
m.model_dump(context={'foo': 'bar'})
assert contexts == []


def test_serialize_json_context() -> None:
contexts: List[Any] = [None, None, {'foo': 'bar'}]

class Model(BaseModel):
x: int

@field_serializer('x')
def serialize_x(self, v: int, info: SerializationInfo) -> int:
assert info.context == contexts.pop(0)
return v

m = Model.model_construct(**{'x': 1})
m.model_dump_json()
m.model_dump_json(context=None)
m.model_dump_json(context={'foo': 'bar'})
assert contexts == []