Skip to content

Commit edad0db

Browse files
fix: keep old behaviour of json() by default (#3542)
* fix: handle basemodel fallback for custom encoders * put back old behaviour and add to_dict * typo Co-authored-by: Christian Bundy <[email protected]> Co-authored-by: Christian Bundy <[email protected]>
1 parent e14e756 commit edad0db

File tree

4 files changed

+62
-7
lines changed

4 files changed

+62
-7
lines changed

docs/examples/exporting_models_json_forward_ref.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ class Config:
3131
User(name='John', address=Address(city='London', country='UK')),
3232
],
3333
)
34-
print(wolfgang.json())
34+
print(wolfgang.json(models_as_dict=False))

docs/usage/exporting_models.md

+3
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ _(This script is complete, it should run "as is")_
109109

110110
### Serialising self-reference or other models
111111

112+
By default, models are serialised as dictionaries.
113+
If you want to serialise them differently, you can add `models_as_dict=False` when calling `json()` method
114+
and add the classes of the model in `json_encoders`.
112115
In case of forward references, you can use a string with the class name instead of the class itself
113116
```py
114117
{!.tmp_examples/exporting_models_json_forward_ref.py!}

pydantic/main.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ def json(
454454
exclude_defaults: bool = False,
455455
exclude_none: bool = False,
456456
encoder: Optional[Callable[[Any], Any]] = None,
457+
models_as_dict: bool = True,
457458
**dumps_kwargs: Any,
458459
) -> str:
459460
"""
@@ -469,11 +470,12 @@ def json(
469470
exclude_unset = skip_defaults
470471
encoder = cast(Callable[[Any], Any], encoder or self.__json_encoder__)
471472

472-
# We don't directly call `self.dict()`, which does exactly the same thing but
473-
# with `to_dict = True` because we want to keep raw `BaseModel` instances and not as `dict`.
473+
# We don't directly call `self.dict()`, which does exactly this with `to_dict=True`
474+
# because we want to be able to keep raw `BaseModel` instances and not as `dict`.
474475
# This allows users to write custom JSON encoders for given `BaseModel` classes.
475476
data = dict(
476477
self._iter(
478+
to_dict=models_as_dict,
477479
by_alias=by_alias,
478480
include=include,
479481
exclude=exclude,

tests/test_json.py

+54-4
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ class Config:
281281
assert m.json() == '{\n "a": 1,\n "b": "foo"\n}'
282282

283283

284-
def test_json_nested_encode():
284+
def test_json_nested_encode_models():
285285
class Phone(BaseModel):
286286
manufacturer: str
287287
number: int
@@ -314,8 +314,58 @@ class Config:
314314

315315
timon.friend = pumbaa
316316

317-
assert iphone.json() == '{"manufacturer": "Apple", "number": 18002752273}'
317+
assert iphone.json(models_as_dict=False) == '{"manufacturer": "Apple", "number": 18002752273}'
318318
assert (
319-
pumbaa.json() == '{"name": "Pumbaa", "SSN": 234, "birthday": 737424000.0, "phone": 18007267864, "friend": null}'
319+
pumbaa.json(models_as_dict=False)
320+
== '{"name": "Pumbaa", "SSN": 234, "birthday": 737424000.0, "phone": 18007267864, "friend": null}'
320321
)
321-
assert timon.json() == '{"name": "Timon", "SSN": 123, "birthday": 738892800.0, "phone": 18002752273, "friend": 234}'
322+
assert (
323+
timon.json(models_as_dict=False)
324+
== '{"name": "Timon", "SSN": 123, "birthday": 738892800.0, "phone": 18002752273, "friend": 234}'
325+
)
326+
327+
328+
def test_custom_encode_fallback_basemodel():
329+
class MyExoticType:
330+
pass
331+
332+
def custom_encoder(o):
333+
if isinstance(o, MyExoticType):
334+
return 'exo'
335+
raise TypeError('not serialisable')
336+
337+
class Foo(BaseModel):
338+
x: MyExoticType
339+
340+
class Config:
341+
arbitrary_types_allowed = True
342+
343+
class Bar(BaseModel):
344+
foo: Foo
345+
346+
assert Bar(foo=Foo(x=MyExoticType())).json(encoder=custom_encoder) == '{"foo": {"x": "exo"}}'
347+
348+
349+
def test_custom_encode_error():
350+
class MyExoticType:
351+
pass
352+
353+
def custom_encoder(o):
354+
raise TypeError('not serialisable')
355+
356+
class Foo(BaseModel):
357+
x: MyExoticType
358+
359+
class Config:
360+
arbitrary_types_allowed = True
361+
362+
with pytest.raises(TypeError, match='not serialisable'):
363+
Foo(x=MyExoticType()).json(encoder=custom_encoder)
364+
365+
366+
def test_recursive():
367+
class Model(BaseModel):
368+
value: Optional[str]
369+
nested: Optional[BaseModel]
370+
371+
assert Model(value=None, nested=Model(value=None)).json(exclude_none=True) == '{"nested": {}}'

0 commit comments

Comments
 (0)