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

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

9 

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

14 

15from .utils import needs_pydanticv1, needs_pydanticv2 1abcdefg

16 

17 

18class Person: 1abcdefg

19 def __init__(self, name: str): 1abcdefg

20 self.name = name 1BhCiDjEkFlGmHn

21 

22 

23class Pet: 1abcdefg

24 def __init__(self, owner: Person, name: str): 1abcdefg

25 self.owner = owner 1BhCiDjEkFlGmHn

26 self.name = name 1BhCiDjEkFlGmHn

27 

28 

29@dataclass 1abcdefg

30class Item: 1abcdefg

31 name: str 1abcdefg

32 count: int 1abcdefg

33 

34 

35class DictablePerson(Person): 1abcdefg

36 def __iter__(self): 1abcdefg

37 return ((k, v) for k, v in self.__dict__.items()) 1hijklmn

38 

39 

40class DictablePet(Pet): 1abcdefg

41 def __iter__(self): 1abcdefg

42 return ((k, v) for k, v in self.__dict__.items()) 1hijklmn

43 

44 

45class Unserializable: 1abcdefg

46 def __iter__(self): 1abcdefg

47 raise NotImplementedError() 2abbbcbdbebfbgb

48 

49 @property 1abcdefg

50 def __dict__(self): 1abcdefg

51 raise NotImplementedError() 2abbbcbdbebfbgb

52 

53 

54class RoleEnum(Enum): 1abcdefg

55 admin = "admin" 1abcdefg

56 normal = "normal" 1abcdefg

57 

58 

59class ModelWithConfig(BaseModel): 1abcdefg

60 role: Optional[RoleEnum] = None 1abcdefg

61 

62 if PYDANTIC_V2: 1abcdefg

63 model_config = {"use_enum_values": True} 1abcdefg

64 else: 

65 

66 class Config: 1abcdef

67 use_enum_values = True 1abcdef

68 

69 

70class ModelWithAlias(BaseModel): 1abcdefg

71 foo: str = Field(alias="Foo") 1abcdefg

72 

73 

74class ModelWithDefault(BaseModel): 1abcdefg

75 foo: str = ... # type: ignore 1abcdefg

76 bar: str = "bar" 1abcdefg

77 bla: str = "bla" 1abcdefg

78 

79 

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 } 

90 

91 

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 } 

103 

104 

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 } 

116 

117 

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[]^_`{|

125 

126 

127def test_encode_unsupported(): 1abcdefg

128 unserializable = Unserializable() 2abbbcbdbebfbgb

129 with pytest.raises(ValueError): 2abbbcbdbebfbgb

130 jsonable_encoder(unserializable) 2abbbcbdbebfbgb

131 

132 

133@needs_pydanticv2 1abcdefg

134def test_encode_custom_json_encoders_model_pydanticv2(): 1abcdefg

135 from pydantic import field_serializer 1opqrstu

136 

137 class ModelWithCustomEncoder(BaseModel): 1opqrstu

138 dt_field: datetime 1opqrstu

139 

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

143 

144 class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): 1opqrstu

145 pass 1opqrstu

146 

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

151 

152 

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

158 

159 class Config: 1vwxyzA

160 json_encoders = { 1vwxyzA

161 datetime: lambda dt: dt.replace( 

162 microsecond=0, tzinfo=timezone.utc 

163 ).isoformat() 

164 } 

165 

166 class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): 1vwxyzA

167 class Config: 1vwxyzA

168 pass 1vwxyzA

169 

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

174 

175 

176def test_encode_model_with_config(): 1abcdefg

177 model = ModelWithConfig(role=RoleEnum.admin) 2BbCbDbEbFbGbHb

178 assert jsonable_encoder(model) == {"role": "admin"} 2BbCbDbEbFbGbHb

179 

180 

181def test_encode_model_with_alias_raises(): 1abcdefg

182 with pytest.raises(ValidationError): 2IbJbKbLbMbNbOb

183 ModelWithAlias(foo="Bar") 2IbJbKbLbMbNbOb

184 

185 

186def test_encode_model_with_alias(): 1abcdefg

187 model = ModelWithAlias(Foo="Bar") 2PbQbRbSbTbUbVb

188 assert jsonable_encoder(model) == {"Foo": "Bar"} 2PbQbRbSbTbUbVb

189 

190 

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 } 

207 

208 

209@needs_pydanticv1 1abcdefg

210def test_custom_encoders(): 1abcdefg

211 class safe_datetime(datetime): 1PQRSTU

212 pass 1PQRSTU

213 

214 class MyModel(BaseModel): 1PQRSTU

215 dt_field: safe_datetime 1PQRSTU

216 

217 instance = MyModel(dt_field=safe_datetime.now()) 1PQRSTU

218 

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

223 

224 encoded_instance2 = jsonable_encoder(instance) 1PQRSTU

225 assert encoded_instance2["dt_field"] == instance.dt_field.isoformat() 1PQRSTU

226 

227 

228def test_custom_enum_encoders(): 1abcdefg

229 def custom_enum_encoder(v: Enum): 1%'()*+,

230 return v.value.lower() 1%'()*+,

231 

232 class MyEnum(Enum): 1%'()*+,

233 ENUM_VAL_1 = "ENUM_VAL_1" 1%'()*+,

234 

235 instance = MyEnum.ENUM_VAL_1 1%'()*+,

236 

237 encoded_instance = jsonable_encoder( 1%'()*+,

238 instance, custom_encoder={MyEnum: custom_enum_encoder} 

239 ) 

240 assert encoded_instance == custom_enum_encoder(instance) 1%'()*+,

241 

242 

243def test_encode_model_with_pure_path(): 1abcdefg

244 class ModelWithPath(BaseModel): 1VWXYZ0-

245 path: PurePath 1VWXYZ0-

246 

247 if PYDANTIC_V2: 1VWXYZ0-

248 model_config = {"arbitrary_types_allowed": True} 1VWXYZ0-

249 else: 

250 

251 class Config: 1VWXYZ0

252 arbitrary_types_allowed = True 1VWXYZ0

253 

254 test_path = PurePath("/foo", "bar") 1VWXYZ0-

255 obj = ModelWithPath(path=test_path) 1VWXYZ0-

256 assert jsonable_encoder(obj) == {"path": str(test_path)} 1VWXYZ0-

257 

258 

259def test_encode_model_with_pure_posix_path(): 1abcdefg

260 class ModelWithPath(BaseModel): 1123456}

261 path: PurePosixPath 1123456}

262 

263 if PYDANTIC_V2: 1123456}

264 model_config = {"arbitrary_types_allowed": True} 1123456}

265 else: 

266 

267 class Config: 1123456

268 arbitrary_types_allowed = True 1123456

269 

270 obj = ModelWithPath(path=PurePosixPath("/foo", "bar")) 1123456}

271 assert jsonable_encoder(obj) == {"path": "/foo/bar"} 1123456}

272 

273 

274def test_encode_model_with_pure_windows_path(): 1abcdefg

275 class ModelWithPath(BaseModel): 1789!#$~

276 path: PureWindowsPath 1789!#$~

277 

278 if PYDANTIC_V2: 1789!#$~

279 model_config = {"arbitrary_types_allowed": True} 1789!#$~

280 else: 

281 

282 class Config: 1789!#$

283 arbitrary_types_allowed = True 1789!#$

284 

285 obj = ModelWithPath(path=PureWindowsPath("/foo", "bar")) 1789!#$~

286 assert jsonable_encoder(obj) == {"path": "\\foo\\bar"} 1789!#$~

287 

288 

289@needs_pydanticv1 1abcdefg

290def test_encode_root(): 1abcdefg

291 class ModelWithRoot(BaseModel): 2hbibjbkblbmb

292 __root__: str 2hbibjbkblbmb

293 

294 model = ModelWithRoot(__root__="Foo") 2hbibjbkblbmb

295 assert jsonable_encoder(model) == "Foo" 2hbibjbkblbmb

296 

297 

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

302 

303 

304@needs_pydanticv2 1abcdefg

305def test_decimal_encoder_int(): 1abcdefg

306 data = {"value": Decimal(2)} 23b4b5b6b7b8b9b

307 assert jsonable_encoder(data) == {"value": 2} 23b4b5b6b7b8b9b

308 

309 

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

314 

315 

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

322 

323 

324def test_encode_deque_encodes_child_models(): 1abcdefg

325 class Model(BaseModel): 2ubvbwbxbybzbAb

326 test: str 2ubvbwbxbybzbAb

327 

328 dq = deque([Model(test="test")]) 2ubvbwbxbybzbAb

329 

330 assert jsonable_encoder(dq)[0]["test"] == "test" 2ubvbwbxbybzbAb

331 

332 

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