Coverage for tests/test_jsonable_encoder.py: 100%

200 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-09-22 00:03 +0000

1from collections import deque 1abcdef

2from dataclasses import dataclass 1abcdef

3from datetime import datetime, timezone 1abcdef

4from decimal import Decimal 1abcdef

5from enum import Enum 1abcdef

6from pathlib import PurePath, PurePosixPath, PureWindowsPath 1abcdef

7from typing import Optional 1abcdef

8 

9import pytest 1abcdef

10from fastapi._compat import PYDANTIC_V2, Undefined 1abcdef

11from fastapi.encoders import jsonable_encoder 1abcdef

12from pydantic import BaseModel, Field, ValidationError 1abcdef

13 

14from .utils import needs_pydanticv1, needs_pydanticv2 1abcdef

15 

16 

17class Person: 1abcdef

18 def __init__(self, name: str): 1abcdef

19 self.name = name 1ygzhAiBjCkDl

20 

21 

22class Pet: 1abcdef

23 def __init__(self, owner: Person, name: str): 1abcdef

24 self.owner = owner 1ygzhAiBjCkDl

25 self.name = name 1ygzhAiBjCkDl

26 

27 

28@dataclass 1abcdef

29class Item: 1abcdef

30 name: str 1abcdef

31 count: int 1abcdef

32 

33 

34class DictablePerson(Person): 1abcdef

35 def __iter__(self): 1abcdef

36 return ((k, v) for k, v in self.__dict__.items()) 1ghijkl

37 

38 

39class DictablePet(Pet): 1abcdef

40 def __iter__(self): 1abcdef

41 return ((k, v) for k, v in self.__dict__.items()) 1ghijkl

42 

43 

44class Unserializable: 1abcdef

45 def __iter__(self): 1abcdef

46 raise NotImplementedError() 1?@[]^_

47 

48 @property 1abcdef

49 def __dict__(self): 1abcdef

50 raise NotImplementedError() 1?@[]^_

51 

52 

53class RoleEnum(Enum): 1abcdef

54 admin = "admin" 1abcdef

55 normal = "normal" 1abcdef

56 

57 

58class ModelWithConfig(BaseModel): 1abcdef

59 role: Optional[RoleEnum] = None 1abcdef

60 

61 if PYDANTIC_V2: 1abcdef

62 model_config = {"use_enum_values": True} 1abcdef

63 else: 

64 

65 class Config: 1abcdef

66 use_enum_values = True 1abcdef

67 

68 

69class ModelWithAlias(BaseModel): 1abcdef

70 foo: str = Field(alias="Foo") 1abcdef

71 

72 

73class ModelWithDefault(BaseModel): 1abcdef

74 foo: str = ... # type: ignore 1abcdef

75 bar: str = "bar" 1abcdef

76 bla: str = "bla" 1abcdef

77 

78 

79def test_encode_dict(): 1abcdef

80 pet = {"name": "Firulais", "owner": {"name": "Foo"}} 1'()*+,

81 assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} 1'()*+,

82 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 1'()*+,

83 assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} 1'()*+,

84 assert jsonable_encoder(pet, include={}) == {} 1'()*+,

85 assert jsonable_encoder(pet, exclude={}) == { 1'()*+,

86 "name": "Firulais", 

87 "owner": {"name": "Foo"}, 

88 } 

89 

90 

91def test_encode_class(): 1abcdef

92 person = Person(name="Foo") 1yzABCD

93 pet = Pet(owner=person, name="Firulais") 1yzABCD

94 assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} 1yzABCD

95 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 1yzABCD

96 assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} 1yzABCD

97 assert jsonable_encoder(pet, include={}) == {} 1yzABCD

98 assert jsonable_encoder(pet, exclude={}) == { 1yzABCD

99 "name": "Firulais", 

100 "owner": {"name": "Foo"}, 

101 } 

102 

103 

104def test_encode_dictable(): 1abcdef

105 person = DictablePerson(name="Foo") 1ghijkl

106 pet = DictablePet(owner=person, name="Firulais") 1ghijkl

107 assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} 1ghijkl

108 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 1ghijkl

109 assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} 1ghijkl

110 assert jsonable_encoder(pet, include={}) == {} 1ghijkl

111 assert jsonable_encoder(pet, exclude={}) == { 1ghijkl

112 "name": "Firulais", 

113 "owner": {"name": "Foo"}, 

114 } 

115 

116 

117def test_encode_dataclass(): 1abcdef

118 item = Item(name="foo", count=100) 1-./:;=

119 assert jsonable_encoder(item) == {"name": "foo", "count": 100} 1-./:;=

120 assert jsonable_encoder(item, include={"name"}) == {"name": "foo"} 1-./:;=

121 assert jsonable_encoder(item, exclude={"count"}) == {"name": "foo"} 1-./:;=

122 assert jsonable_encoder(item, include={}) == {} 1-./:;=

123 assert jsonable_encoder(item, exclude={}) == {"name": "foo", "count": 100} 1-./:;=

124 

125 

126def test_encode_unsupported(): 1abcdef

127 unserializable = Unserializable() 1?@[]^_

128 with pytest.raises(ValueError): 1?@[]^_

129 jsonable_encoder(unserializable) 1?@[]^_

130 

131 

132@needs_pydanticv2 1abcdef

133def test_encode_custom_json_encoders_model_pydanticv2(): 1abcdef

134 from pydantic import field_serializer 1mnopqr

135 

136 class ModelWithCustomEncoder(BaseModel): 1mnopqr

137 dt_field: datetime 1mnopqr

138 

139 @field_serializer("dt_field") 1mnopqr

140 def serialize_dt_field(self, dt): 1mnopqr

141 return dt.replace(microsecond=0, tzinfo=timezone.utc).isoformat() 1mnopqr

142 

143 class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): 1mnopqr

144 pass 1mnopqr

145 

146 model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) 1mnopqr

147 assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1mnopqr

148 subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) 1mnopqr

149 assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1mnopqr

150 

151 

152# TODO: remove when deprecating Pydantic v1 

153@needs_pydanticv1 1abcdef

154def test_encode_custom_json_encoders_model_pydanticv1(): 1abcdef

155 class ModelWithCustomEncoder(BaseModel): 1stuvwx

156 dt_field: datetime 1stuvwx

157 

158 class Config: 1stuvwx

159 json_encoders = { 1stuvwx

160 datetime: lambda dt: dt.replace( 

161 microsecond=0, tzinfo=timezone.utc 

162 ).isoformat() 

163 } 

164 

165 class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): 1stuvwx

166 class Config: 1stuvwx

167 pass 1stuvwx

168 

169 model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) 1stuvwx

170 assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1stuvwx

171 subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) 1stuvwx

172 assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1stuvwx

173 

174 

175def test_encode_model_with_config(): 1abcdef

176 model = ModelWithConfig(role=RoleEnum.admin) 2hbibjbkblbmb

177 assert jsonable_encoder(model) == {"role": "admin"} 2hbibjbkblbmb

178 

179 

180def test_encode_model_with_alias_raises(): 1abcdef

181 with pytest.raises(ValidationError): 2nbobpbqbrbsb

182 ModelWithAlias(foo="Bar") 2nbobpbqbrbsb

183 

184 

185def test_encode_model_with_alias(): 1abcdef

186 model = ModelWithAlias(Foo="Bar") 2tbubvbwbxbyb

187 assert jsonable_encoder(model) == {"Foo": "Bar"} 2tbubvbwbxbyb

188 

189 

190def test_encode_model_with_default(): 1abcdef

191 model = ModelWithDefault(foo="foo", bar="bar") 1EFGHIJ

192 assert jsonable_encoder(model) == {"foo": "foo", "bar": "bar", "bla": "bla"} 1EFGHIJ

193 assert jsonable_encoder(model, exclude_unset=True) == {"foo": "foo", "bar": "bar"} 1EFGHIJ

194 assert jsonable_encoder(model, exclude_defaults=True) == {"foo": "foo"} 1EFGHIJ

195 assert jsonable_encoder(model, exclude_unset=True, exclude_defaults=True) == { 1EFGHIJ

196 "foo": "foo" 

197 } 

198 assert jsonable_encoder(model, include={"foo"}) == {"foo": "foo"} 1EFGHIJ

199 assert jsonable_encoder(model, exclude={"bla"}) == {"foo": "foo", "bar": "bar"} 1EFGHIJ

200 assert jsonable_encoder(model, include={}) == {} 1EFGHIJ

201 assert jsonable_encoder(model, exclude={}) == { 1EFGHIJ

202 "foo": "foo", 

203 "bar": "bar", 

204 "bla": "bla", 

205 } 

206 

207 

208@needs_pydanticv1 1abcdef

209def test_custom_encoders(): 1abcdef

210 class safe_datetime(datetime): 1KLMNOP

211 pass 1KLMNOP

212 

213 class MyModel(BaseModel): 1KLMNOP

214 dt_field: safe_datetime 1KLMNOP

215 

216 instance = MyModel(dt_field=safe_datetime.now()) 1KLMNOP

217 

218 encoded_instance = jsonable_encoder( 1KLMNOP

219 instance, custom_encoder={safe_datetime: lambda o: o.strftime("%H:%M:%S")} 

220 ) 

221 assert encoded_instance["dt_field"] == instance.dt_field.strftime("%H:%M:%S") 1KLMNOP

222 

223 encoded_instance2 = jsonable_encoder(instance) 1KLMNOP

224 assert encoded_instance2["dt_field"] == instance.dt_field.isoformat() 1KLMNOP

225 

226 

227def test_custom_enum_encoders(): 1abcdef

228 def custom_enum_encoder(v: Enum): 189!#$%

229 return v.value.lower() 189!#$%

230 

231 class MyEnum(Enum): 189!#$%

232 ENUM_VAL_1 = "ENUM_VAL_1" 189!#$%

233 

234 instance = MyEnum.ENUM_VAL_1 189!#$%

235 

236 encoded_instance = jsonable_encoder( 189!#$%

237 instance, custom_encoder={MyEnum: custom_enum_encoder} 

238 ) 

239 assert encoded_instance == custom_enum_encoder(instance) 189!#$%

240 

241 

242def test_encode_model_with_pure_path(): 1abcdef

243 class ModelWithPath(BaseModel): 1QRSTUV

244 path: PurePath 1QRSTUV

245 

246 if PYDANTIC_V2: 1QRSTUV

247 model_config = {"arbitrary_types_allowed": True} 1QRSTUV

248 else: 

249 

250 class Config: 1QRSTUV

251 arbitrary_types_allowed = True 1QRSTUV

252 

253 test_path = PurePath("/foo", "bar") 1QRSTUV

254 obj = ModelWithPath(path=test_path) 1QRSTUV

255 assert jsonable_encoder(obj) == {"path": str(test_path)} 1QRSTUV

256 

257 

258def test_encode_model_with_pure_posix_path(): 1abcdef

259 class ModelWithPath(BaseModel): 1WXYZ01

260 path: PurePosixPath 1WXYZ01

261 

262 if PYDANTIC_V2: 1WXYZ01

263 model_config = {"arbitrary_types_allowed": True} 1WXYZ01

264 else: 

265 

266 class Config: 1WXYZ01

267 arbitrary_types_allowed = True 1WXYZ01

268 

269 obj = ModelWithPath(path=PurePosixPath("/foo", "bar")) 1WXYZ01

270 assert jsonable_encoder(obj) == {"path": "/foo/bar"} 1WXYZ01

271 

272 

273def test_encode_model_with_pure_windows_path(): 1abcdef

274 class ModelWithPath(BaseModel): 1234567

275 path: PureWindowsPath 1234567

276 

277 if PYDANTIC_V2: 1234567

278 model_config = {"arbitrary_types_allowed": True} 1234567

279 else: 

280 

281 class Config: 1234567

282 arbitrary_types_allowed = True 1234567

283 

284 obj = ModelWithPath(path=PureWindowsPath("/foo", "bar")) 1234567

285 assert jsonable_encoder(obj) == {"path": "\\foo\\bar"} 1234567

286 

287 

288@needs_pydanticv1 1abcdef

289def test_encode_root(): 1abcdef

290 class ModelWithRoot(BaseModel): 2` { | } ~ ab

291 __root__: str 2` { | } ~ ab

292 

293 model = ModelWithRoot(__root__="Foo") 2` { | } ~ ab

294 assert jsonable_encoder(model) == "Foo" 2` { | } ~ ab

295 

296 

297@needs_pydanticv2 1abcdef

298def test_decimal_encoder_float(): 1abcdef

299 data = {"value": Decimal(1.23)} 2zbAbBbCbDbEb

300 assert jsonable_encoder(data) == {"value": 1.23} 2zbAbBbCbDbEb

301 

302 

303@needs_pydanticv2 1abcdef

304def test_decimal_encoder_int(): 1abcdef

305 data = {"value": Decimal(2)} 2FbGbHbIbJbKb

306 assert jsonable_encoder(data) == {"value": 2} 2FbGbHbIbJbKb

307 

308 

309def test_encode_deque_encodes_child_models(): 1abcdef

310 class Model(BaseModel): 2bbcbdbebfbgb

311 test: str 2bbcbdbebfbgb

312 

313 dq = deque([Model(test="test")]) 2bbcbdbebfbgb

314 

315 assert jsonable_encoder(dq)[0]["test"] == "test" 2bbcbdbebfbgb

316 

317 

318@needs_pydanticv2 1abcdef

319def test_encode_pydantic_undefined(): 1abcdef

320 data = {"value": Undefined} 2LbMbNbObPbQb

321 assert jsonable_encoder(data) == {"value": None} 2LbMbNbObPbQb