Skip to content

Commit

Permalink
fix: keep old behaviour of json() by default (#3542)
Browse files Browse the repository at this point in the history
* fix: handle basemodel fallback for custom encoders

* put back old behaviour and add to_dict

* typo

Co-authored-by: Christian Bundy <christianbundy@fraction.io>

Co-authored-by: Christian Bundy <christianbundy@fraction.io>
  • Loading branch information
PrettyWood and christianbundy committed Dec 24, 2021
1 parent e14e756 commit edad0db
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 7 deletions.
2 changes: 1 addition & 1 deletion docs/examples/exporting_models_json_forward_ref.py
Expand Up @@ -31,4 +31,4 @@ class Config:
User(name='John', address=Address(city='London', country='UK')),
],
)
print(wolfgang.json())
print(wolfgang.json(models_as_dict=False))
3 changes: 3 additions & 0 deletions docs/usage/exporting_models.md
Expand Up @@ -109,6 +109,9 @@ _(This script is complete, it should run "as is")_

### Serialising self-reference or other models

By default, models are serialised as dictionaries.
If you want to serialise them differently, you can add `models_as_dict=False` when calling `json()` method
and add the classes of the model in `json_encoders`.
In case of forward references, you can use a string with the class name instead of the class itself
```py
{!.tmp_examples/exporting_models_json_forward_ref.py!}
Expand Down
6 changes: 4 additions & 2 deletions pydantic/main.py
Expand Up @@ -454,6 +454,7 @@ def json(
exclude_defaults: bool = False,
exclude_none: bool = False,
encoder: Optional[Callable[[Any], Any]] = None,
models_as_dict: bool = True,
**dumps_kwargs: Any,
) -> str:
"""
Expand All @@ -469,11 +470,12 @@ def json(
exclude_unset = skip_defaults
encoder = cast(Callable[[Any], Any], encoder or self.__json_encoder__)

# We don't directly call `self.dict()`, which does exactly the same thing but
# with `to_dict = True` because we want to keep raw `BaseModel` instances and not as `dict`.
# We don't directly call `self.dict()`, which does exactly this with `to_dict=True`
# because we want to be able to keep raw `BaseModel` instances and not as `dict`.
# This allows users to write custom JSON encoders for given `BaseModel` classes.
data = dict(
self._iter(
to_dict=models_as_dict,
by_alias=by_alias,
include=include,
exclude=exclude,
Expand Down
58 changes: 54 additions & 4 deletions tests/test_json.py
Expand Up @@ -281,7 +281,7 @@ class Config:
assert m.json() == '{\n "a": 1,\n "b": "foo"\n}'


def test_json_nested_encode():
def test_json_nested_encode_models():
class Phone(BaseModel):
manufacturer: str
number: int
Expand Down Expand Up @@ -314,8 +314,58 @@ class Config:

timon.friend = pumbaa

assert iphone.json() == '{"manufacturer": "Apple", "number": 18002752273}'
assert iphone.json(models_as_dict=False) == '{"manufacturer": "Apple", "number": 18002752273}'
assert (
pumbaa.json() == '{"name": "Pumbaa", "SSN": 234, "birthday": 737424000.0, "phone": 18007267864, "friend": null}'
pumbaa.json(models_as_dict=False)
== '{"name": "Pumbaa", "SSN": 234, "birthday": 737424000.0, "phone": 18007267864, "friend": null}'
)
assert timon.json() == '{"name": "Timon", "SSN": 123, "birthday": 738892800.0, "phone": 18002752273, "friend": 234}'
assert (
timon.json(models_as_dict=False)
== '{"name": "Timon", "SSN": 123, "birthday": 738892800.0, "phone": 18002752273, "friend": 234}'
)


def test_custom_encode_fallback_basemodel():
class MyExoticType:
pass

def custom_encoder(o):
if isinstance(o, MyExoticType):
return 'exo'
raise TypeError('not serialisable')

class Foo(BaseModel):
x: MyExoticType

class Config:
arbitrary_types_allowed = True

class Bar(BaseModel):
foo: Foo

assert Bar(foo=Foo(x=MyExoticType())).json(encoder=custom_encoder) == '{"foo": {"x": "exo"}}'


def test_custom_encode_error():
class MyExoticType:
pass

def custom_encoder(o):
raise TypeError('not serialisable')

class Foo(BaseModel):
x: MyExoticType

class Config:
arbitrary_types_allowed = True

with pytest.raises(TypeError, match='not serialisable'):
Foo(x=MyExoticType()).json(encoder=custom_encoder)


def test_recursive():
class Model(BaseModel):
value: Optional[str]
nested: Optional[BaseModel]

assert Model(value=None, nested=Model(value=None)).json(exclude_none=True) == '{"nested": {}}'

0 comments on commit edad0db

Please sign in to comment.