Coverage for tests/test_jsonable_encoder.py: 100%
211 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-12-04 08:29 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2025-12-04 08:29 +0000
1from collections import deque 1abcdefg
2from dataclasses import dataclass 1abcdefg
3from datetime import datetime, timezone 1abcdefg
4from decimal import Decimal 1abcdefg
5from enum import Enum 1abcdefg
6from math import isinf, isnan 1abcdefg
7from pathlib import PurePath, PurePosixPath, PureWindowsPath 1abcdefg
8from typing import Optional 1abcdefg
10import pytest 1abcdefg
11from fastapi._compat import PYDANTIC_V2, Undefined 1abcdefg
12from fastapi.encoders import jsonable_encoder 1abcdefg
13from pydantic import BaseModel, Field, ValidationError 1abcdefg
15from .utils import needs_pydanticv1, needs_pydanticv2 1abcdefg
18class Person: 1abcdefg
19 def __init__(self, name: str): 1abcdefg
20 self.name = name 1BhCiDjEkFlGmHn
23class Pet: 1abcdefg
24 def __init__(self, owner: Person, name: str): 1abcdefg
25 self.owner = owner 1BhCiDjEkFlGmHn
26 self.name = name 1BhCiDjEkFlGmHn
29@dataclass 1abcdefg
30class Item: 1abcdefg
31 name: str 1abcdefg
32 count: int 1abcdefg
35class DictablePerson(Person): 1abcdefg
36 def __iter__(self): 1abcdefg
37 return ((k, v) for k, v in self.__dict__.items()) 1hijklmn
40class DictablePet(Pet): 1abcdefg
41 def __iter__(self): 1abcdefg
42 return ((k, v) for k, v in self.__dict__.items()) 1hijklmn
45class Unserializable: 1abcdefg
46 def __iter__(self): 1abcdefg
47 raise NotImplementedError() 2abbbcbdbebfbgb
49 @property 1abcdefg
50 def __dict__(self): 1abcdefg
51 raise NotImplementedError() 2abbbcbdbebfbgb
54class RoleEnum(Enum): 1abcdefg
55 admin = "admin" 1abcdefg
56 normal = "normal" 1abcdefg
59class ModelWithConfig(BaseModel): 1abcdefg
60 role: Optional[RoleEnum] = None 1abcdefg
62 if PYDANTIC_V2: 1abcdefg
63 model_config = {"use_enum_values": True} 1abcdefg
64 else:
66 class Config: 1abcdef
67 use_enum_values = True 1abcdef
70class ModelWithAlias(BaseModel): 1abcdefg
71 foo: str = Field(alias="Foo") 1abcdefg
74class ModelWithDefault(BaseModel): 1abcdefg
75 foo: str = ... # type: ignore 1abcdefg
76 bar: str = "bar" 1abcdefg
77 bla: str = "bla" 1abcdefg
80def test_encode_dict(): 1abcdefg
81 pet = {"name": "Firulais", "owner": {"name": "Foo"}} 1./:;=?@
82 assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} 1./:;=?@
83 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 1./:;=?@
84 assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} 1./:;=?@
85 assert jsonable_encoder(pet, include={}) == {} 1./:;=?@
86 assert jsonable_encoder(pet, exclude={}) == { 1./:;=?@
87 "name": "Firulais",
88 "owner": {"name": "Foo"},
89 }
92def test_encode_class(): 1abcdefg
93 person = Person(name="Foo") 1BCDEFGH
94 pet = Pet(owner=person, name="Firulais") 1BCDEFGH
95 assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} 1BCDEFGH
96 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 1BCDEFGH
97 assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} 1BCDEFGH
98 assert jsonable_encoder(pet, include={}) == {} 1BCDEFGH
99 assert jsonable_encoder(pet, exclude={}) == { 1BCDEFGH
100 "name": "Firulais",
101 "owner": {"name": "Foo"},
102 }
105def test_encode_dictable(): 1abcdefg
106 person = DictablePerson(name="Foo") 1hijklmn
107 pet = DictablePet(owner=person, name="Firulais") 1hijklmn
108 assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} 1hijklmn
109 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 1hijklmn
110 assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} 1hijklmn
111 assert jsonable_encoder(pet, include={}) == {} 1hijklmn
112 assert jsonable_encoder(pet, exclude={}) == { 1hijklmn
113 "name": "Firulais",
114 "owner": {"name": "Foo"},
115 }
118def test_encode_dataclass(): 1abcdefg
119 item = Item(name="foo", count=100) 1[]^_`{|
120 assert jsonable_encoder(item) == {"name": "foo", "count": 100} 1[]^_`{|
121 assert jsonable_encoder(item, include={"name"}) == {"name": "foo"} 1[]^_`{|
122 assert jsonable_encoder(item, exclude={"count"}) == {"name": "foo"} 1[]^_`{|
123 assert jsonable_encoder(item, include={}) == {} 1[]^_`{|
124 assert jsonable_encoder(item, exclude={}) == {"name": "foo", "count": 100} 1[]^_`{|
127def test_encode_unsupported(): 1abcdefg
128 unserializable = Unserializable() 2abbbcbdbebfbgb
129 with pytest.raises(ValueError): 2abbbcbdbebfbgb
130 jsonable_encoder(unserializable) 2abbbcbdbebfbgb
133@needs_pydanticv2 1abcdefg
134def test_encode_custom_json_encoders_model_pydanticv2(): 1abcdefg
135 from pydantic import field_serializer 1opqrstu
137 class ModelWithCustomEncoder(BaseModel): 1opqrstu
138 dt_field: datetime 1opqrstu
140 @field_serializer("dt_field") 1opqrstu
141 def serialize_dt_field(self, dt): 1opqrstu
142 return dt.replace(microsecond=0, tzinfo=timezone.utc).isoformat() 1opqrstu
144 class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): 1opqrstu
145 pass 1opqrstu
147 model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) 1opqrstu
148 assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1opqrstu
149 subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) 1opqrstu
150 assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1opqrstu
153# TODO: remove when deprecating Pydantic v1
154@needs_pydanticv1 1abcdefg
155def test_encode_custom_json_encoders_model_pydanticv1(): 1abcdefg
156 class ModelWithCustomEncoder(BaseModel): 1vwxyzA
157 dt_field: datetime 1vwxyzA
159 class Config: 1vwxyzA
160 json_encoders = { 1vwxyzA
161 datetime: lambda dt: dt.replace(
162 microsecond=0, tzinfo=timezone.utc
163 ).isoformat()
164 }
166 class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): 1vwxyzA
167 class Config: 1vwxyzA
168 pass 1vwxyzA
170 model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) 1vwxyzA
171 assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1vwxyzA
172 subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) 1vwxyzA
173 assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1vwxyzA
176def test_encode_model_with_config(): 1abcdefg
177 model = ModelWithConfig(role=RoleEnum.admin) 2BbCbDbEbFbGbHb
178 assert jsonable_encoder(model) == {"role": "admin"} 2BbCbDbEbFbGbHb
181def test_encode_model_with_alias_raises(): 1abcdefg
182 with pytest.raises(ValidationError): 2IbJbKbLbMbNbOb
183 ModelWithAlias(foo="Bar") 2IbJbKbLbMbNbOb
186def test_encode_model_with_alias(): 1abcdefg
187 model = ModelWithAlias(Foo="Bar") 2PbQbRbSbTbUbVb
188 assert jsonable_encoder(model) == {"Foo": "Bar"} 2PbQbRbSbTbUbVb
191def test_encode_model_with_default(): 1abcdefg
192 model = ModelWithDefault(foo="foo", bar="bar") 1IJKLMNO
193 assert jsonable_encoder(model) == {"foo": "foo", "bar": "bar", "bla": "bla"} 1IJKLMNO
194 assert jsonable_encoder(model, exclude_unset=True) == {"foo": "foo", "bar": "bar"} 1IJKLMNO
195 assert jsonable_encoder(model, exclude_defaults=True) == {"foo": "foo"} 1IJKLMNO
196 assert jsonable_encoder(model, exclude_unset=True, exclude_defaults=True) == { 1IJKLMNO
197 "foo": "foo"
198 }
199 assert jsonable_encoder(model, include={"foo"}) == {"foo": "foo"} 1IJKLMNO
200 assert jsonable_encoder(model, exclude={"bla"}) == {"foo": "foo", "bar": "bar"} 1IJKLMNO
201 assert jsonable_encoder(model, include={}) == {} 1IJKLMNO
202 assert jsonable_encoder(model, exclude={}) == { 1IJKLMNO
203 "foo": "foo",
204 "bar": "bar",
205 "bla": "bla",
206 }
209@needs_pydanticv1 1abcdefg
210def test_custom_encoders(): 1abcdefg
211 class safe_datetime(datetime): 1PQRSTU
212 pass 1PQRSTU
214 class MyModel(BaseModel): 1PQRSTU
215 dt_field: safe_datetime 1PQRSTU
217 instance = MyModel(dt_field=safe_datetime.now()) 1PQRSTU
219 encoded_instance = jsonable_encoder( 1PQRSTU
220 instance, custom_encoder={safe_datetime: lambda o: o.strftime("%H:%M:%S")}
221 )
222 assert encoded_instance["dt_field"] == instance.dt_field.strftime("%H:%M:%S") 1PQRSTU
224 encoded_instance2 = jsonable_encoder(instance) 1PQRSTU
225 assert encoded_instance2["dt_field"] == instance.dt_field.isoformat() 1PQRSTU
228def test_custom_enum_encoders(): 1abcdefg
229 def custom_enum_encoder(v: Enum): 1%'()*+,
230 return v.value.lower() 1%'()*+,
232 class MyEnum(Enum): 1%'()*+,
233 ENUM_VAL_1 = "ENUM_VAL_1" 1%'()*+,
235 instance = MyEnum.ENUM_VAL_1 1%'()*+,
237 encoded_instance = jsonable_encoder( 1%'()*+,
238 instance, custom_encoder={MyEnum: custom_enum_encoder}
239 )
240 assert encoded_instance == custom_enum_encoder(instance) 1%'()*+,
243def test_encode_model_with_pure_path(): 1abcdefg
244 class ModelWithPath(BaseModel): 1VWXYZ0-
245 path: PurePath 1VWXYZ0-
247 if PYDANTIC_V2: 1VWXYZ0-
248 model_config = {"arbitrary_types_allowed": True} 1VWXYZ0-
249 else:
251 class Config: 1VWXYZ0
252 arbitrary_types_allowed = True 1VWXYZ0
254 test_path = PurePath("/foo", "bar") 1VWXYZ0-
255 obj = ModelWithPath(path=test_path) 1VWXYZ0-
256 assert jsonable_encoder(obj) == {"path": str(test_path)} 1VWXYZ0-
259def test_encode_model_with_pure_posix_path(): 1abcdefg
260 class ModelWithPath(BaseModel): 1123456}
261 path: PurePosixPath 1123456}
263 if PYDANTIC_V2: 1123456}
264 model_config = {"arbitrary_types_allowed": True} 1123456}
265 else:
267 class Config: 1123456
268 arbitrary_types_allowed = True 1123456
270 obj = ModelWithPath(path=PurePosixPath("/foo", "bar")) 1123456}
271 assert jsonable_encoder(obj) == {"path": "/foo/bar"} 1123456}
274def test_encode_model_with_pure_windows_path(): 1abcdefg
275 class ModelWithPath(BaseModel): 1789!#$~
276 path: PureWindowsPath 1789!#$~
278 if PYDANTIC_V2: 1789!#$~
279 model_config = {"arbitrary_types_allowed": True} 1789!#$~
280 else:
282 class Config: 1789!#$
283 arbitrary_types_allowed = True 1789!#$
285 obj = ModelWithPath(path=PureWindowsPath("/foo", "bar")) 1789!#$~
286 assert jsonable_encoder(obj) == {"path": "\\foo\\bar"} 1789!#$~
289@needs_pydanticv1 1abcdefg
290def test_encode_root(): 1abcdefg
291 class ModelWithRoot(BaseModel): 2hbibjbkblbmb
292 __root__: str 2hbibjbkblbmb
294 model = ModelWithRoot(__root__="Foo") 2hbibjbkblbmb
295 assert jsonable_encoder(model) == "Foo" 2hbibjbkblbmb
298@needs_pydanticv2 1abcdefg
299def test_decimal_encoder_float(): 1abcdefg
300 data = {"value": Decimal(1.23)} 2WbXbYbZb0b1b2b
301 assert jsonable_encoder(data) == {"value": 1.23} 2WbXbYbZb0b1b2b
304@needs_pydanticv2 1abcdefg
305def test_decimal_encoder_int(): 1abcdefg
306 data = {"value": Decimal(2)} 23b4b5b6b7b8b9b
307 assert jsonable_encoder(data) == {"value": 2} 23b4b5b6b7b8b9b
310@needs_pydanticv2 1abcdefg
311def test_decimal_encoder_nan(): 1abcdefg
312 data = {"value": Decimal("NaN")} 2!b#b$b%b'b(b)b
313 assert isnan(jsonable_encoder(data)["value"]) 2!b#b$b%b'b(b)b
316@needs_pydanticv2 1abcdefg
317def test_decimal_encoder_infinity(): 1abcdefg
318 data = {"value": Decimal("Infinity")} 2nbobpbqbrbsbtb
319 assert isinf(jsonable_encoder(data)["value"]) 2nbobpbqbrbsbtb
320 data = {"value": Decimal("-Infinity")} 2nbobpbqbrbsbtb
321 assert isinf(jsonable_encoder(data)["value"]) 2nbobpbqbrbsbtb
324def test_encode_deque_encodes_child_models(): 1abcdefg
325 class Model(BaseModel): 2ubvbwbxbybzbAb
326 test: str 2ubvbwbxbybzbAb
328 dq = deque([Model(test="test")]) 2ubvbwbxbybzbAb
330 assert jsonable_encoder(dq)[0]["test"] == "test" 2ubvbwbxbybzbAb
333@needs_pydanticv2 1abcdefg
334def test_encode_pydantic_undefined(): 1abcdefg
335 data = {"value": Undefined} 2*b+b,b-b.b/b:b
336 assert jsonable_encoder(data) == {"value": None} 2*b+b,b-b.b/b:b