Coverage for tests / test_jsonable_encoder.py: 100%

195 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-12 18:15 +0000

1import warnings 1abcd

2from collections import deque 1abcd

3from dataclasses import dataclass 1abcd

4from datetime import datetime, timezone 1abcd

5from decimal import Decimal 1abcd

6from enum import Enum 1abcd

7from math import isinf, isnan 1abcd

8from pathlib import PurePath, PurePosixPath, PureWindowsPath 1abcd

9from typing import Optional, TypedDict 1abcd

10 

11import pytest 1abcd

12from fastapi._compat import Undefined 1abcd

13from fastapi.encoders import jsonable_encoder 1abcd

14from fastapi.exceptions import PydanticV1NotSupportedError 1abcd

15from pydantic import BaseModel, Field, ValidationError 1abcd

16 

17 

18class Person: 1abcd

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

20 self.name = name 1neofpg

21 

22 

23class Pet: 1abcd

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

25 self.owner = owner 1neofpg

26 self.name = name 1neofpg

27 

28 

29@dataclass 1abcd

30class Item: 1abcd

31 name: str 1abcd

32 count: int 1abcd

33 

34 

35class DictablePerson(Person): 1abcd

36 def __iter__(self): 1abcd

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

38 

39 

40class DictablePet(Pet): 1abcd

41 def __iter__(self): 1abcd

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

43 

44 

45class Unserializable: 1abcd

46 def __iter__(self): 1abcd

47 raise NotImplementedError() 1LMN

48 

49 @property 1abcd

50 def __dict__(self): 1abcd

51 raise NotImplementedError() 1LMN

52 

53 

54class RoleEnum(Enum): 1abcd

55 admin = "admin" 1abcd

56 normal = "normal" 1abcd

57 

58 

59class ModelWithConfig(BaseModel): 1abcd

60 role: Optional[RoleEnum] = None 1abcd

61 

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

63 

64 

65class ModelWithAlias(BaseModel): 1abcd

66 foo: str = Field(alias="Foo") 1abcd

67 

68 

69class ModelWithDefault(BaseModel): 1abcd

70 foo: str = ... # type: ignore 1abcd

71 bar: str = "bar" 1abcd

72 bla: str = "bla" 1abcd

73 

74 

75def test_encode_dict(): 1abcd

76 pet = {"name": "Firulais", "owner": {"name": "Foo"}} 1zAB

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

78 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 1zAB

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

80 assert jsonable_encoder(pet, include={}) == {} 1zAB

81 assert jsonable_encoder(pet, exclude={}) == { 1zAB

82 "name": "Firulais", 

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

84 } 

85 

86 

87def test_encode_dict_include_exclude_list(): 1abcd

88 pet = {"name": "Firulais", "owner": {"name": "Foo"}} 1CDE

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

90 assert jsonable_encoder(pet, include=["name"]) == {"name": "Firulais"} 1CDE

91 assert jsonable_encoder(pet, exclude=["owner"]) == {"name": "Firulais"} 1CDE

92 assert jsonable_encoder(pet, include=[]) == {} 1CDE

93 assert jsonable_encoder(pet, exclude=[]) == { 1CDE

94 "name": "Firulais", 

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

96 } 

97 

98 

99def test_encode_class(): 1abcd

100 person = Person(name="Foo") 1nop

101 pet = Pet(owner=person, name="Firulais") 1nop

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

103 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 1nop

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

105 assert jsonable_encoder(pet, include={}) == {} 1nop

106 assert jsonable_encoder(pet, exclude={}) == { 1nop

107 "name": "Firulais", 

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

109 } 

110 

111 

112def test_encode_dictable(): 1abcd

113 person = DictablePerson(name="Foo") 1efg

114 pet = DictablePet(owner=person, name="Firulais") 1efg

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

116 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 1efg

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

118 assert jsonable_encoder(pet, include={}) == {} 1efg

119 assert jsonable_encoder(pet, exclude={}) == { 1efg

120 "name": "Firulais", 

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

122 } 

123 

124 

125def test_encode_dataclass(): 1abcd

126 item = Item(name="foo", count=100) 1FGH

127 assert jsonable_encoder(item) == {"name": "foo", "count": 100} 1FGH

128 assert jsonable_encoder(item, include={"name"}) == {"name": "foo"} 1FGH

129 assert jsonable_encoder(item, exclude={"count"}) == {"name": "foo"} 1FGH

130 assert jsonable_encoder(item, include={}) == {} 1FGH

131 assert jsonable_encoder(item, exclude={}) == {"name": "foo", "count": 100} 1FGH

132 

133 

134def test_encode_unsupported(): 1abcd

135 unserializable = Unserializable() 1LMN

136 with pytest.raises(ValueError): 1LMN

137 jsonable_encoder(unserializable) 1LMN

138 

139 

140def test_encode_custom_json_encoders_model_pydanticv2(): 1abcd

141 from pydantic import field_serializer 1hij

142 

143 class ModelWithCustomEncoder(BaseModel): 1hij

144 dt_field: datetime 1hij

145 

146 @field_serializer("dt_field") 1hij

147 def serialize_dt_field(self, dt): 1hij

148 return dt.replace(microsecond=0, tzinfo=timezone.utc).isoformat() 1hij

149 

150 class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): 1hij

151 pass 1hij

152 

153 model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) 1hij

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

155 subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) 1hij

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

157 

158 

159def test_json_encoder_error_with_pydanticv1(): 1abcd

160 with warnings.catch_warnings(): 1tvu

161 warnings.simplefilter("ignore", UserWarning) 1tvu

162 from pydantic import v1 1tvu

163 

164 class ModelV1(v1.BaseModel): 1tvu

165 name: str 1tu

166 

167 data = ModelV1(name="test") 1tvu

168 with pytest.raises(PydanticV1NotSupportedError): 1tvu

169 jsonable_encoder(data) 1tvu

170 

171 

172def test_encode_model_with_config(): 1abcd

173 model = ModelWithConfig(role=RoleEnum.admin) 1012

174 assert jsonable_encoder(model) == {"role": "admin"} 1012

175 

176 

177def test_encode_model_with_alias_raises(): 1abcd

178 with pytest.raises(ValidationError): 1345

179 ModelWithAlias(foo="Bar") 1345

180 

181 

182def test_encode_model_with_alias(): 1abcd

183 model = ModelWithAlias(Foo="Bar") 1678

184 assert jsonable_encoder(model) == {"Foo": "Bar"} 1678

185 

186 

187def test_encode_model_with_default(): 1abcd

188 model = ModelWithDefault(foo="foo", bar="bar") 1qrs

189 assert jsonable_encoder(model) == {"foo": "foo", "bar": "bar", "bla": "bla"} 1qrs

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

191 assert jsonable_encoder(model, exclude_defaults=True) == {"foo": "foo"} 1qrs

192 assert jsonable_encoder(model, exclude_unset=True, exclude_defaults=True) == { 1qrs

193 "foo": "foo" 

194 } 

195 assert jsonable_encoder(model, include={"foo"}) == {"foo": "foo"} 1qrs

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

197 assert jsonable_encoder(model, include={}) == {} 1qrs

198 assert jsonable_encoder(model, exclude={}) == { 1qrs

199 "foo": "foo", 

200 "bar": "bar", 

201 "bla": "bla", 

202 } 

203 

204 

205def test_custom_encoders(): 1abcd

206 class safe_datetime(datetime): 1klm

207 pass 1klm

208 

209 class MyDict(TypedDict): 1klm

210 dt_field: safe_datetime 1klm

211 

212 instance = MyDict(dt_field=safe_datetime.now()) 1klm

213 

214 encoded_instance = jsonable_encoder( 1klm

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

216 ) 

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

218 

219 encoded_instance = jsonable_encoder( 1klm

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

221 ) 

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

223 

224 encoded_instance2 = jsonable_encoder(instance) 1klm

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

226 

227 

228def test_custom_enum_encoders(): 1abcd

229 def custom_enum_encoder(v: Enum): 1wxy

230 return v.value.lower() 1wxy

231 

232 class MyEnum(Enum): 1wxy

233 ENUM_VAL_1 = "ENUM_VAL_1" 1wxy

234 

235 instance = MyEnum.ENUM_VAL_1 1wxy

236 

237 encoded_instance = jsonable_encoder( 1wxy

238 instance, custom_encoder={MyEnum: custom_enum_encoder} 

239 ) 

240 assert encoded_instance == custom_enum_encoder(instance) 1wxy

241 

242 

243def test_encode_model_with_pure_path(): 1abcd

244 class ModelWithPath(BaseModel): 1IJK

245 path: PurePath 1IJK

246 

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

248 

249 test_path = PurePath("/foo", "bar") 1IJK

250 obj = ModelWithPath(path=test_path) 1IJK

251 assert jsonable_encoder(obj) == {"path": str(test_path)} 1IJK

252 

253 

254def test_encode_model_with_pure_posix_path(): 1abcd

255 class ModelWithPath(BaseModel): 1OPQ

256 path: PurePosixPath 1OPQ

257 

258 model_config = {"arbitrary_types_allowed": True} 1OPQ

259 

260 obj = ModelWithPath(path=PurePosixPath("/foo", "bar")) 1OPQ

261 assert jsonable_encoder(obj) == {"path": "/foo/bar"} 1OPQ

262 

263 

264def test_encode_model_with_pure_windows_path(): 1abcd

265 class ModelWithPath(BaseModel): 1RST

266 path: PureWindowsPath 1RST

267 

268 model_config = {"arbitrary_types_allowed": True} 1RST

269 

270 obj = ModelWithPath(path=PureWindowsPath("/foo", "bar")) 1RST

271 assert jsonable_encoder(obj) == {"path": "\\foo\\bar"} 1RST

272 

273 

274def test_encode_pure_path(): 1abcd

275 test_path = PurePath("/foo", "bar") 19!#

276 

277 assert jsonable_encoder({"path": test_path}) == {"path": str(test_path)} 19!#

278 

279 

280def test_decimal_encoder_float(): 1abcd

281 data = {"value": Decimal(1.23)} 1$%'

282 assert jsonable_encoder(data) == {"value": 1.23} 1$%'

283 

284 

285def test_decimal_encoder_int(): 1abcd

286 data = {"value": Decimal(2)} 1()*

287 assert jsonable_encoder(data) == {"value": 2} 1()*

288 

289 

290def test_decimal_encoder_nan(): 1abcd

291 data = {"value": Decimal("NaN")} 1+,-

292 assert isnan(jsonable_encoder(data)["value"]) 1+,-

293 

294 

295def test_decimal_encoder_infinity(): 1abcd

296 data = {"value": Decimal("Infinity")} 1UVW

297 assert isinf(jsonable_encoder(data)["value"]) 1UVW

298 data = {"value": Decimal("-Infinity")} 1UVW

299 assert isinf(jsonable_encoder(data)["value"]) 1UVW

300 

301 

302def test_encode_deque_encodes_child_models(): 1abcd

303 class Model(BaseModel): 1XYZ

304 test: str 1XYZ

305 

306 dq = deque([Model(test="test")]) 1XYZ

307 

308 assert jsonable_encoder(dq)[0]["test"] == "test" 1XYZ

309 

310 

311def test_encode_pydantic_undefined(): 1abcd

312 data = {"value": Undefined} 1./:

313 assert jsonable_encoder(data) == {"value": None} 1./: