Skip to content

Commit ce8d618

Browse files
committed
support __pydantic_extra__ when serializing
1 parent d3f97e7 commit ce8d618

File tree

2 files changed

+55
-8
lines changed

2 files changed

+55
-8
lines changed

src/serializers/shared.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::fmt::Debug;
33

44
use pyo3::exceptions::PyTypeError;
55
use pyo3::prelude::*;
6-
use pyo3::types::{PyDict, PySet};
6+
use pyo3::types::{PyDict, PyMapping, PySet};
77
use pyo3::{intern, PyTraverseError, PyVisit};
88

99
use enum_dispatch::enum_dispatch;
@@ -329,17 +329,36 @@ pub(super) fn object_to_dict<'py>(value: &'py PyAny, is_model: bool, extra: &Ext
329329
let py = value.py();
330330
let attr = value.getattr(intern!(py, "__dict__"))?;
331331
let attrs: &PyDict = attr.downcast()?;
332-
if is_model && extra.exclude_unset {
333-
let fields_set: &PySet = value.getattr(intern!(py, "__pydantic_fields_set__"))?.downcast()?;
332+
if is_model {
333+
if let Some(extra) = get_pydantic_extra(value) {
334+
attrs.update(extra)?;
335+
}
336+
337+
if extra.exclude_unset {
338+
let fields_set: &PySet = value.getattr(intern!(py, "__pydantic_fields_set__"))?.downcast()?;
334339

335-
let new_attrs = attrs.copy()?;
336-
for key in new_attrs.keys() {
337-
if !fields_set.contains(key)? {
338-
new_attrs.del_item(key)?;
340+
let new_attrs = attrs.copy()?;
341+
for key in new_attrs.keys() {
342+
if !fields_set.contains(key)? {
343+
new_attrs.del_item(key)?;
344+
}
339345
}
346+
Ok(new_attrs)
347+
} else {
348+
Ok(attrs)
340349
}
341-
Ok(new_attrs)
342350
} else {
343351
Ok(attrs)
344352
}
345353
}
354+
355+
fn get_pydantic_extra(value: &PyAny) -> Option<&PyMapping> {
356+
let extra = match value.getattr(intern!(value.py(), "__pydantic_extra__")) {
357+
Ok(extra) => extra,
358+
Err(_) => return None,
359+
};
360+
match extra.downcast::<PyMapping>() {
361+
Ok(attrs) => Some(attrs),
362+
Err(_) => None,
363+
}
364+
}

tests/serializers/test_model.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,3 +741,31 @@ def random_n(self) -> int:
741741
del sq.area
742742
assert s.to_python(sq, by_alias=False) == {'side': 0, 'area': 0, 'random_n': the_random_n}
743743
assert s.to_python(sq, exclude={'random_n'}) == {'side': 0, 'area': 0}
744+
745+
746+
def test_extra():
747+
class MyModel:
748+
# this is not required, but it avoids `__pydantic_fields_set__` being included in `__dict__`
749+
__slots__ = '__dict__', '__pydantic_extra__', '__pydantic_fields_set__'
750+
field_a: str
751+
field_b: int
752+
753+
schema = core_schema.model_schema(
754+
MyModel,
755+
core_schema.model_fields_schema(
756+
{
757+
'field_a': core_schema.model_field(core_schema.str_schema()),
758+
'field_b': core_schema.model_field(core_schema.int_schema()),
759+
},
760+
extra_behavior='allow',
761+
),
762+
)
763+
v = SchemaValidator(schema)
764+
m = v.validate_python({'field_a': 'test', 'field_b': 12, 'field_c': 'extra'})
765+
assert isinstance(m, MyModel)
766+
assert m.__dict__ == {'field_a': 'test', 'field_b': 12}
767+
assert m.__pydantic_extra__ == {'field_c': 'extra'}
768+
assert m.__pydantic_fields_set__ == {'field_a', 'field_b', 'field_c'}
769+
770+
s = SchemaSerializer(schema)
771+
assert s.to_python(m) == {'field_a': 'test', 'field_b': 12, 'field_c': 'extra'}

0 commit comments

Comments
 (0)