diff --git a/docs/examples/exporting_models_json_forward_ref.py b/docs/examples/exporting_models_json_forward_ref.py index 540221f3a2..0ae9eb947f 100644 --- a/docs/examples/exporting_models_json_forward_ref.py +++ b/docs/examples/exporting_models_json_forward_ref.py @@ -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)) diff --git a/docs/usage/exporting_models.md b/docs/usage/exporting_models.md index f5770daa4f..1673c45d53 100644 --- a/docs/usage/exporting_models.md +++ b/docs/usage/exporting_models.md @@ -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!} diff --git a/pydantic/main.py b/pydantic/main.py index fa501701f7..a00cc0a3e1 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -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: """ @@ -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, diff --git a/tests/test_json.py b/tests/test_json.py index 9ef3892839..5871446b75 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -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 @@ -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": {}}'