Skip to content

Commit 5b51a8a

Browse files
committed
refactor according to comments
1 parent 7222c8d commit 5b51a8a

File tree

2 files changed

+213
-91
lines changed

2 files changed

+213
-91
lines changed

src/serializers/fields.rs

Lines changed: 132 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -156,59 +156,28 @@ impl GeneralFieldsSerializer {
156156
let output_dict = PyDict::new(py);
157157
let mut used_req_fields: usize = 0;
158158

159-
let mut items = main_iter.collect::<PyResult<Vec<_>>>()?;
160-
if extra.sort_keys {
161-
items.sort_by_cached_key(|(key, _)| key_str(key).unwrap_or_default().to_string());
162-
}
163-
164-
for (key, value) in items {
165-
let key_str = key_str(&key)?;
166-
let op_field = self.fields.get(key_str);
167-
if extra.exclude_none && value.is_none() {
168-
if let Some(field) = op_field {
169-
if field.required {
159+
if !extra.sort_keys {
160+
for result in main_iter {
161+
let (key, value) = result?;
162+
if let Some(is_required) =
163+
self.process_field_entry_python(&key, &value, &output_dict, include, exclude, &extra)?
164+
{
165+
if is_required {
170166
used_req_fields += 1;
171167
}
172168
}
173-
continue;
174169
}
175-
let field_extra = Extra {
176-
field_name: Some(key_str),
177-
..extra
178-
};
179-
if let Some((next_include, next_exclude)) = self.filter.key_filter(&key, include, exclude)? {
180-
if let Some(field) = op_field {
181-
if let Some(ref serializer) = field.serializer {
182-
if !exclude_default(&value, &field_extra, serializer)? {
183-
let value = serializer.to_python(
184-
&value,
185-
next_include.as_ref(),
186-
next_exclude.as_ref(),
187-
&field_extra,
188-
)?;
189-
let output_key = field.get_key_py(output_dict.py(), &field_extra);
190-
output_dict.set_item(output_key, value)?;
191-
}
192-
}
170+
} else {
171+
let mut items = main_iter.collect::<PyResult<Vec<_>>>()?;
172+
items.sort_by_cached_key(|(key, _)| key_str(key).unwrap_or_default().to_string());
193173

194-
if field.required {
174+
for (key, value) in items {
175+
if let Some(is_required) =
176+
self.process_field_entry_python(&key, &value, &output_dict, include, exclude, &extra)?
177+
{
178+
if is_required {
195179
used_req_fields += 1;
196180
}
197-
} else if self.mode == FieldsMode::TypedDictAllow {
198-
let value = match &self.extra_serializer {
199-
Some(serializer) => {
200-
serializer.to_python(&value, next_include.as_ref(), next_exclude.as_ref(), &field_extra)?
201-
}
202-
None => infer_to_python(&value, next_include.as_ref(), next_exclude.as_ref(), &field_extra)?,
203-
};
204-
output_dict.set_item(key, value)?;
205-
} else if field_extra.check == SerCheck::Strict {
206-
return Err(PydanticSerializationUnexpectedValue::new(
207-
Some(format!("Unexpected field `{key}`")),
208-
field_extra.model_type_name().map(|bound| bound.to_string()),
209-
None,
210-
)
211-
.to_py_err());
212181
}
213182
}
214183
}
@@ -232,6 +201,73 @@ impl GeneralFieldsSerializer {
232201
}
233202
}
234203

204+
fn process_field_entry_python<'py>(
205+
&self,
206+
key: &Bound<'py, PyAny>,
207+
value: &Bound<'py, PyAny>,
208+
output_dict: &Bound<'py, PyDict>,
209+
include: Option<&Bound<'py, PyAny>>,
210+
exclude: Option<&Bound<'py, PyAny>>,
211+
extra: &Extra,
212+
) -> PyResult<Option<bool>> {
213+
// This function updates output_dict directly and returns:
214+
// - Some(true) -> Field was required and processed
215+
// - Some(false) -> Field was processed but not required
216+
// - None -> Field was filtered out or skipped
217+
let key_str = key_str(key)?;
218+
let op_field = self.fields.get(key_str);
219+
220+
if extra.exclude_none && value.is_none() {
221+
if let Some(field) = op_field {
222+
if field.required {
223+
return Ok(Some(true));
224+
}
225+
}
226+
return Ok(None);
227+
}
228+
229+
let field_extra = Extra {
230+
field_name: Some(key_str),
231+
..*extra
232+
};
233+
234+
if let Some((next_include, next_exclude)) = self.filter.key_filter(key, include, exclude)? {
235+
if let Some(field) = op_field {
236+
if let Some(ref serializer) = field.serializer {
237+
if !exclude_default(value, &field_extra, serializer)? {
238+
let value =
239+
serializer.to_python(value, next_include.as_ref(), next_exclude.as_ref(), &field_extra)?;
240+
let output_key = field.get_key_py(output_dict.py(), &field_extra);
241+
output_dict.set_item(output_key, value)?;
242+
}
243+
}
244+
245+
if field.required {
246+
return Ok(Some(true));
247+
}
248+
return Ok(Some(false));
249+
} else if self.mode == FieldsMode::TypedDictAllow {
250+
let value = match &self.extra_serializer {
251+
Some(serializer) => {
252+
serializer.to_python(value, next_include.as_ref(), next_exclude.as_ref(), &field_extra)?
253+
}
254+
None => infer_to_python(value, next_include.as_ref(), next_exclude.as_ref(), &field_extra)?,
255+
};
256+
output_dict.set_item(key, value)?;
257+
return Ok(None);
258+
} else if field_extra.check == SerCheck::Strict {
259+
return Err(PydanticSerializationUnexpectedValue::new(
260+
Some(format!("Unexpected field `{key}`")),
261+
field_extra.model_type_name().map(|bound| bound.to_string()),
262+
None,
263+
)
264+
.to_py_err());
265+
}
266+
}
267+
268+
Ok(None)
269+
}
270+
235271
pub(crate) fn main_serde_serialize<'py, S: serde::ser::Serializer>(
236272
&self,
237273
main_iter: impl Iterator<Item = PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)>>,
@@ -245,45 +281,63 @@ impl GeneralFieldsSerializer {
245281
// we don't both with `used_fields` here because on unions, `to_python(..., mode='json')` is used
246282
let mut map = serializer.serialize_map(Some(expected_len))?;
247283

248-
let mut items = main_iter.collect::<PyResult<Vec<_>>>().map_err(py_err_se_err)?;
249-
if extra.sort_keys {
284+
if !extra.sort_keys {
285+
for result in main_iter {
286+
let (key, value) = result.map_err(py_err_se_err)?;
287+
self.process_field_entry::<S>(&key, &value, &mut map, include, exclude, &extra)?;
288+
}
289+
} else {
290+
let mut items = main_iter.collect::<PyResult<Vec<_>>>().map_err(py_err_se_err)?;
250291
items.sort_by_cached_key(|(key, _)| key_str(key).unwrap_or_default().to_string());
251-
}
252-
for (key, value) in items {
253-
if extra.exclude_none && value.is_none() {
254-
continue;
292+
for (key, value) in items {
293+
self.process_field_entry::<S>(&key, &value, &mut map, include, exclude, &extra)?;
255294
}
256-
let key_str = key_str(&key).map_err(py_err_se_err)?;
257-
let field_extra = Extra {
258-
field_name: Some(key_str),
259-
..extra
260-
};
295+
}
296+
Ok(map)
297+
}
261298

262-
let filter = self.filter.key_filter(&key, include, exclude).map_err(py_err_se_err)?;
263-
if let Some((next_include, next_exclude)) = filter {
264-
if let Some(field) = self.fields.get(key_str) {
265-
if let Some(ref serializer) = field.serializer {
266-
if !exclude_default(&value, &field_extra, serializer).map_err(py_err_se_err)? {
267-
let s = PydanticSerializer::new(
268-
&value,
269-
serializer,
270-
next_include.as_ref(),
271-
next_exclude.as_ref(),
272-
&field_extra,
273-
);
274-
let output_key = field.get_key_json(key_str, &field_extra);
275-
map.serialize_entry(&output_key, &s)?;
276-
}
299+
fn process_field_entry<'py, S: serde::ser::Serializer>(
300+
&self,
301+
key: &Bound<'py, PyAny>,
302+
value: &Bound<'py, PyAny>,
303+
map: &mut S::SerializeMap,
304+
include: Option<&Bound<'py, PyAny>>,
305+
exclude: Option<&Bound<'py, PyAny>>,
306+
extra: &Extra,
307+
) -> Result<(), S::Error> {
308+
if extra.exclude_none && value.is_none() {
309+
return Ok(());
310+
}
311+
let key_str = key_str(key).map_err(py_err_se_err)?;
312+
let field_extra = Extra {
313+
field_name: Some(key_str),
314+
..*extra
315+
};
316+
317+
let filter = self.filter.key_filter(key, include, exclude).map_err(py_err_se_err)?;
318+
if let Some((next_include, next_exclude)) = filter {
319+
if let Some(field) = self.fields.get(key_str) {
320+
if let Some(ref serializer) = field.serializer {
321+
if !exclude_default(value, &field_extra, serializer).map_err(py_err_se_err)? {
322+
let s = PydanticSerializer::new(
323+
value,
324+
serializer,
325+
next_include.as_ref(),
326+
next_exclude.as_ref(),
327+
&field_extra,
328+
);
329+
let output_key = field.get_key_json(key_str, &field_extra);
330+
map.serialize_entry(&output_key, &s)?;
277331
}
278-
} else if self.mode == FieldsMode::TypedDictAllow {
279-
let output_key = infer_json_key(&key, &field_extra).map_err(py_err_se_err)?;
280-
let s = SerializeInfer::new(&value, next_include.as_ref(), next_exclude.as_ref(), &field_extra);
281-
map.serialize_entry(&output_key, &s)?;
282332
}
283-
// no error case here since unions (which need the error case) use `to_python(..., mode='json')`
333+
} else if self.mode == FieldsMode::TypedDictAllow {
334+
let output_key = infer_json_key(key, &field_extra).map_err(py_err_se_err)?;
335+
let s = SerializeInfer::new(value, next_include.as_ref(), next_exclude.as_ref(), &field_extra);
336+
map.serialize_entry(&output_key, &s)?;
284337
}
338+
// no error case here since unions (which need the error case) use `to_python(..., mode='json')`
285339
}
286-
Ok(map)
340+
Ok(())
287341
}
288342

289343
pub(crate) fn add_computed_fields_python(

tests/serializers/test_model.py

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -992,22 +992,22 @@ def test_extra():
992992
class MyModel:
993993
# this is not required, but it avoids `__pydantic_fields_set__` being included in `__dict__`
994994
__slots__ = '__dict__', '__pydantic_fields_set__', '__pydantic_extra__', '__pydantic_private__'
995-
field_b: int
996995
field_a: str
996+
field_b: int
997997

998998
schema = core_schema.model_schema(
999999
MyModel,
10001000
core_schema.model_fields_schema(
10011001
{
1002-
'field_b': core_schema.model_field(core_schema.int_schema()),
10031002
'field_a': core_schema.model_field(core_schema.bytes_schema()),
1003+
'field_b': core_schema.model_field(core_schema.int_schema()),
10041004
},
10051005
extra_behavior='allow',
10061006
),
10071007
extra_behavior='allow',
10081008
)
10091009
v = SchemaValidator(schema)
1010-
m = v.validate_python({'field_b': 12, 'field_a': b'test', 'field_c': 'extra'})
1010+
m = v.validate_python({'field_a': b'test', 'field_b': 12, 'field_c': 'extra'})
10111011
assert isinstance(m, MyModel)
10121012
assert m.__dict__ == {'field_a': b'test', 'field_b': 12}
10131013
assert m.__pydantic_extra__ == {'field_c': 'extra'}
@@ -1016,36 +1016,104 @@ class MyModel:
10161016
s = SchemaSerializer(schema)
10171017
assert 'mode:ModelExtra' in plain_repr(s)
10181018
assert 'has_extra:true' in plain_repr(s)
1019-
assert s.to_python(m, sort_keys=False) == snapshot({'field_a': b'test', 'field_b': 12, 'field_c': 'extra'})
1020-
assert s.to_json(m, sort_keys=True) == b'{"field_a":"test","field_b":12,"field_c":"extra"}'
1019+
assert s.to_python(m) == {'field_a': b'test', 'field_b': 12, 'field_c': 'extra'}
10211020
assert s.to_python(m, mode='json') == {'field_a': 'test', 'field_b': 12, 'field_c': 'extra'}
1021+
assert s.to_json(m) == b'{"field_a":"test","field_b":12,"field_c":"extra"}'
10221022

1023-
# # test filtering
1023+
# test filtering
10241024
m = v.validate_python({'field_a': b'test', 'field_b': 12, 'field_c': None, 'field_d': [1, 2, 3]})
10251025
assert isinstance(m, MyModel)
10261026
assert m.__dict__ == {'field_a': b'test', 'field_b': 12}
10271027
assert m.__pydantic_extra__ == {'field_c': None, 'field_d': [1, 2, 3]}
10281028
assert m.__pydantic_fields_set__ == {'field_a', 'field_b', 'field_c', 'field_d'}
10291029

10301030
assert s.to_python(m) == {'field_a': b'test', 'field_b': 12, 'field_c': None, 'field_d': [1, 2, 3]}
1031-
assert s.to_json(m) == b'{"field_b":12,"field_a":"test","field_c":null,"field_d":[1,2,3]}'
1031+
assert s.to_json(m) == b'{"field_a":"test","field_b":12,"field_c":null,"field_d":[1,2,3]}'
10321032

10331033
assert s.to_python(m, exclude_none=True) == {'field_a': b'test', 'field_b': 12, 'field_d': [1, 2, 3]}
1034-
assert s.to_json(m, exclude_none=True) == b'{"field_b":12,"field_a":"test","field_d":[1,2,3]}'
1034+
assert s.to_json(m, exclude_none=True) == b'{"field_a":"test","field_b":12,"field_d":[1,2,3]}'
10351035

10361036
assert s.to_python(m, exclude={'field_c'}) == {'field_a': b'test', 'field_b': 12, 'field_d': [1, 2, 3]}
1037-
assert s.to_json(m, exclude={'field_c'}) == b'{"field_b":12,"field_a":"test","field_d":[1,2,3]}'
1037+
assert s.to_json(m, exclude={'field_c'}) == b'{"field_a":"test","field_b":12,"field_d":[1,2,3]}'
10381038

10391039
assert s.to_python(m, exclude={'field_d': [0]}) == {
10401040
'field_a': b'test',
10411041
'field_b': 12,
10421042
'field_c': None,
10431043
'field_d': [2, 3],
10441044
}
1045-
assert s.to_json(m, exclude={'field_d': [0]}) == b'{"field_b":12,"field_a":"test","field_c":null,"field_d":[2,3]}'
1046-
assert (
1047-
s.to_json(m, exclude={'field_d': [0]}, sort_keys=True)
1048-
== b'{"field_a":"test","field_b":12,"field_c":null,"field_d":[2,3]}'
1045+
assert s.to_json(m, exclude={'field_d': [0]}) == b'{"field_a":"test","field_b":12,"field_c":null,"field_d":[2,3]}'
1046+
1047+
1048+
def test_extra_sort_keys():
1049+
class MyModel:
1050+
field_123: str
1051+
field_b: int
1052+
field_a: str
1053+
1054+
schema = core_schema.model_schema(
1055+
MyModel,
1056+
core_schema.model_fields_schema(
1057+
{
1058+
'field_123': core_schema.model_field(core_schema.bytes_schema()),
1059+
'field_b': core_schema.model_field(core_schema.int_schema()),
1060+
'field_a': core_schema.model_field(core_schema.bytes_schema()),
1061+
},
1062+
extra_behavior='allow',
1063+
),
1064+
extra_behavior='allow',
1065+
)
1066+
v = SchemaValidator(schema)
1067+
m = v.validate_python({'field_123': b'test_123', 'field_b': 12, 'field_a': b'test', 'field_c': 'extra'})
1068+
s = SchemaSerializer(schema)
1069+
assert 'mode:ModelExtra' in plain_repr(s)
1070+
assert 'has_extra:true' in plain_repr(s)
1071+
assert s.to_json(m, sort_keys=True) == snapshot(
1072+
b'{"field_123":"test_123","field_a":"test","field_b":12,"field_c":"extra"}'
1073+
)
1074+
assert s.to_python(m, mode='json', sort_keys=True) == snapshot(
1075+
{'field_123': 'test_123', 'field_a': 'test', 'field_b': 12, 'field_c': 'extra'}
1076+
)
1077+
1078+
# test filtering
1079+
m = v.validate_python(
1080+
{'field_123': b'test_123', 'field_b': 12, 'field_a': b'test', 'field_c': None, 'field_d': [1, 2, 3]}
1081+
)
1082+
assert s.to_python(m, exclude_none=True) == snapshot(
1083+
{'field_123': b'test_123', 'field_a': b'test', 'field_b': 12, 'field_d': [1, 2, 3]}
1084+
)
1085+
assert s.to_python(m, exclude_none=True, sort_keys=True) == snapshot(
1086+
{'field_123': b'test_123', 'field_a': b'test', 'field_b': 12, 'field_d': [1, 2, 3]}
1087+
)
1088+
assert s.to_json(m, exclude_none=True) == snapshot(
1089+
b'{"field_123":"test_123","field_b":12,"field_a":"test","field_d":[1,2,3]}'
1090+
)
1091+
assert s.to_json(m, exclude_none=True, sort_keys=True) == snapshot(
1092+
b'{"field_123":"test_123","field_a":"test","field_b":12,"field_d":[1,2,3]}'
1093+
)
1094+
assert s.to_python(m, exclude={'field_c'}) == snapshot(
1095+
{'field_123': b'test_123', 'field_a': b'test', 'field_b': 12, 'field_d': [1, 2, 3]}
1096+
)
1097+
assert s.to_python(m, exclude={'field_c'}, sort_keys=True) == snapshot(
1098+
{'field_123': b'test_123', 'field_a': b'test', 'field_b': 12, 'field_d': [1, 2, 3]}
1099+
)
1100+
assert s.to_json(m, exclude={'field_c'}) == snapshot(
1101+
b'{"field_123":"test_123","field_b":12,"field_a":"test","field_d":[1,2,3]}'
1102+
)
1103+
assert s.to_json(m, exclude={'field_c'}, sort_keys=True) == snapshot(
1104+
b'{"field_123":"test_123","field_a":"test","field_b":12,"field_d":[1,2,3]}'
1105+
)
1106+
assert s.to_python(m, exclude={'field_d': [0]}) == snapshot(
1107+
{'field_123': b'test_123', 'field_a': b'test', 'field_b': 12, 'field_c': None, 'field_d': [2, 3]}
1108+
)
1109+
assert s.to_python(m, exclude={'field_d': [0]}, sort_keys=True) == snapshot(
1110+
{'field_123': b'test_123', 'field_a': b'test', 'field_b': 12, 'field_c': None, 'field_d': [2, 3]}
1111+
)
1112+
assert s.to_json(m, exclude={'field_d': [0]}) == snapshot(
1113+
b'{"field_123":"test_123","field_b":12,"field_a":"test","field_c":null,"field_d":[2,3]}'
1114+
)
1115+
assert s.to_json(m, exclude={'field_d': [0]}, sort_keys=True) == snapshot(
1116+
b'{"field_123":"test_123","field_a":"test","field_b":12,"field_c":null,"field_d":[2,3]}'
10491117
)
10501118

10511119

0 commit comments

Comments
 (0)