Skip to content

Commit e416bf8

Browse files
committed
support __pydantic_extra__ when serializing
1 parent b6eff4c commit e416bf8

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;
@@ -353,17 +353,36 @@ pub(super) fn object_to_dict<'py>(value: &'py PyAny, is_model: bool, extra: &Ext
353353
let py = value.py();
354354
let attr = value.getattr(intern!(py, "__dict__"))?;
355355
let attrs: &PyDict = attr.downcast()?;
356-
if is_model && extra.exclude_unset {
357-
let fields_set: &PySet = value.getattr(intern!(py, "__pydantic_fields_set__"))?.downcast()?;
356+
if is_model {
357+
if let Some(extra) = get_pydantic_extra(value) {
358+
attrs.update(extra)?;
359+
}
360+
361+
if extra.exclude_unset {
362+
let fields_set: &PySet = value.getattr(intern!(py, "__pydantic_fields_set__"))?.downcast()?;
358363

359-
let new_attrs = attrs.copy()?;
360-
for key in new_attrs.keys() {
361-
if !fields_set.contains(key)? {
362-
new_attrs.del_item(key)?;
364+
let new_attrs = attrs.copy()?;
365+
for key in new_attrs.keys() {
366+
if !fields_set.contains(key)? {
367+
new_attrs.del_item(key)?;
368+
}
363369
}
370+
Ok(new_attrs)
371+
} else {
372+
Ok(attrs)
364373
}
365-
Ok(new_attrs)
366374
} else {
367375
Ok(attrs)
368376
}
369377
}
378+
379+
fn get_pydantic_extra(value: &PyAny) -> Option<&PyMapping> {
380+
let extra = match value.getattr(intern!(value.py(), "__pydantic_extra__")) {
381+
Ok(extra) => extra,
382+
Err(_) => return None,
383+
};
384+
match extra.downcast::<PyMapping>() {
385+
Ok(attrs) => Some(attrs),
386+
Err(_) => None,
387+
}
388+
}

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)