Coverage for tests/test_jsonable_encoder.py: 100%

198 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-05-05 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): 1234567

211 pass 1234567

212 

213 class MyModel(BaseModel): 1234567

214 dt_field: safe_datetime 1234567

215 

216 instance = MyModel(dt_field=safe_datetime.now()) 1234567

217 

218 encoded_instance = jsonable_encoder( 1234567

219 instance, custom_encoder={safe_datetime: lambda o: o.isoformat()} 

220 ) 

221 assert encoded_instance["dt_field"] == instance.dt_field.isoformat() 1234567

222 

223 

224def test_custom_enum_encoders(): 1abcdef

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

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

227 

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

229 ENUM_VAL_1 = "ENUM_VAL_1" 189!#$%

230 

231 instance = MyEnum.ENUM_VAL_1 189!#$%

232 

233 encoded_instance = jsonable_encoder( 189!#$%

234 instance, custom_encoder={MyEnum: custom_enum_encoder} 

235 ) 

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

237 

238 

239def test_encode_model_with_pure_path(): 1abcdef

240 class ModelWithPath(BaseModel): 1KLMNOP

241 path: PurePath 1KLMNOP

242 

243 if PYDANTIC_V2: 1KLMNOP

244 model_config = {"arbitrary_types_allowed": True} 1KLMNOP

245 else: 

246 

247 class Config: 1KLMNOP

248 arbitrary_types_allowed = True 1KLMNOP

249 

250 test_path = PurePath("/foo", "bar") 1KLMNOP

251 obj = ModelWithPath(path=test_path) 1KLMNOP

252 assert jsonable_encoder(obj) == {"path": str(test_path)} 1KLMNOP

253 

254 

255def test_encode_model_with_pure_posix_path(): 1abcdef

256 class ModelWithPath(BaseModel): 1QRSTUV

257 path: PurePosixPath 1QRSTUV

258 

259 if PYDANTIC_V2: 1QRSTUV

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

261 else: 

262 

263 class Config: 1QRSTUV

264 arbitrary_types_allowed = True 1QRSTUV

265 

266 obj = ModelWithPath(path=PurePosixPath("/foo", "bar")) 1QRSTUV

267 assert jsonable_encoder(obj) == {"path": "/foo/bar"} 1QRSTUV

268 

269 

270def test_encode_model_with_pure_windows_path(): 1abcdef

271 class ModelWithPath(BaseModel): 1WXYZ01

272 path: PureWindowsPath 1WXYZ01

273 

274 if PYDANTIC_V2: 1WXYZ01

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

276 else: 

277 

278 class Config: 1WXYZ01

279 arbitrary_types_allowed = True 1WXYZ01

280 

281 obj = ModelWithPath(path=PureWindowsPath("/foo", "bar")) 1WXYZ01

282 assert jsonable_encoder(obj) == {"path": "\\foo\\bar"} 1WXYZ01

283 

284 

285@needs_pydanticv1 1abcdef

286def test_encode_root(): 1abcdef

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

288 __root__: str 2` { | } ~ ab

289 

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

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

292 

293 

294@needs_pydanticv2 1abcdef

295def test_decimal_encoder_float(): 1abcdef

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

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

298 

299 

300@needs_pydanticv2 1abcdef

301def test_decimal_encoder_int(): 1abcdef

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

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

304 

305 

306def test_encode_deque_encodes_child_models(): 1abcdef

307 class Model(BaseModel): 2bbcbdbebfbgb

308 test: str 2bbcbdbebfbgb

309 

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

311 

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

313 

314 

315@needs_pydanticv2 1abcdef

316def test_encode_pydantic_undefined(): 1abcdef

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

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