Skip to content

Commit

Permalink
Ability to pass context to serialization (fix #7143) (#8965)
Browse files Browse the repository at this point in the history
Co-authored-by: ornariece <37-ornariece@users.noreply.git.malined.com>
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
  • Loading branch information
3 people committed Mar 26, 2024
1 parent e934638 commit b9ec63f
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 0 deletions.
34 changes: 34 additions & 0 deletions docs/concepts/serialization.md
Expand Up @@ -874,6 +874,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`
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'}
```

Similarly, you can [use a context for validation](../concepts/validators.md#validation-context).

## `model_copy(...)`

Expand Down
2 changes: 2 additions & 0 deletions docs/concepts/validators.md
Expand Up @@ -709,6 +709,8 @@ except ValidationError as exc:
"""
```

Similarly, you can [use a context for serialization](../concepts/serialization.md#serialization-context).

### Using validation context with `BaseModel` initialization
Although there is no way to specify a context in the standard `BaseModel` initializer, you can work around this through
the use of `contextvars.ContextVar` and a custom `__init__` method:
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 @@ -310,6 +311,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 @@ -327,6 +329,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 @@ -341,6 +344,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 @@ -357,6 +361,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 @@ -373,6 +378,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
1 change: 1 addition & 0 deletions pydantic/root_model.py
Expand Up @@ -124,6 +124,7 @@ def model_dump( # type: ignore
mode: Literal['json', 'python'] | str = 'python',
include: Any = None,
exclude: Any = None,
context: dict[str, Any] | None = None,
by_alias: bool = False,
exclude_unset: bool = False,
exclude_defaults: bool = False,
Expand Down
36 changes: 36 additions & 0 deletions tests/test_serialize.py
Expand Up @@ -1149,6 +1149,42 @@ class Foo(BaseModel):
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 == []


def test_plain_serializer_with_std_type() -> None:
"""Ensure that a plain serializer can be used with a standard type constructor, rather than having to use lambda x: std_type(x)."""

Expand Down

0 comments on commit b9ec63f

Please sign in to comment.