diff --git a/src/serializers/errors.rs b/src/serializers/errors.rs index 4ed590894..054ac537b 100644 --- a/src/serializers/errors.rs +++ b/src/serializers/errors.rs @@ -99,6 +99,7 @@ impl PydanticSerializationError { #[derive(Debug, Clone)] pub struct PydanticSerializationUnexpectedValue { message: Option, + field_name: Option, field_type: Option, input_value: Option>, } @@ -107,30 +108,43 @@ impl PydanticSerializationUnexpectedValue { pub fn new_from_msg(message: Option) -> Self { Self { message, + field_name: None, field_type: None, input_value: None, } } - pub fn new_from_parts(field_type: Option, input_value: Option>) -> Self { + pub fn new_from_parts( + field_name: Option, + field_type: Option, + input_value: Option>, + ) -> Self { Self { message: None, + field_name, field_type, input_value, } } - pub fn new(message: Option, field_type: Option, input_value: Option>) -> Self { + pub fn new( + message: Option, + field_name: Option, + field_type: Option, + input_value: Option>, + ) -> Self { Self { message, + field_name, field_type, input_value, } } pub fn to_py_err(&self) -> PyErr { - PyErr::new::, Option, Option>)>(( + PyErr::new::, Option, Option, Option>)>(( self.message.clone(), + self.field_name.clone(), self.field_type.clone(), self.input_value.clone(), )) @@ -140,10 +154,16 @@ impl PydanticSerializationUnexpectedValue { #[pymethods] impl PydanticSerializationUnexpectedValue { #[new] - #[pyo3(signature = (message=None, field_type=None, input_value=None, /))] - fn py_new(message: Option, field_type: Option, input_value: Option>) -> Self { + #[pyo3(signature = (message=None, field_name=None, field_type=None, input_value=None, /))] + fn py_new( + message: Option, + field_name: Option, + field_type: Option, + input_value: Option>, + ) -> Self { Self { message, + field_name, field_type, input_value, } @@ -172,8 +192,16 @@ impl PydanticSerializationUnexpectedValue { let value_str = truncate_safe_repr(bound_input, None); - write!(message, " [input_value={value_str}, input_type={input_type}]") + if let Some(field_name) = &self.field_name { + write!( + message, + " [field_name={field_name}, input_value={value_str}, input_type={input_type}]" + ) .expect("writing to string should never fail"); + } else { + write!(message, " [input_value={value_str}, input_type={input_type}]") + .expect("writing to string should never fail"); + } } if message.is_empty() { diff --git a/src/serializers/extra.rs b/src/serializers/extra.rs index 2b4b07b2d..ca9755cc5 100644 --- a/src/serializers/extra.rs +++ b/src/serializers/extra.rs @@ -1,6 +1,7 @@ use std::convert::Infallible; use std::ffi::CString; use std::fmt; +use std::string::ToString; use std::sync::Mutex; use pyo3::exceptions::{PyTypeError, PyUserWarning, PyValueError}; @@ -383,12 +384,13 @@ impl CollectWarnings { Ok(()) } else if extra.check.enabled() { Err(PydanticSerializationUnexpectedValue::new_from_parts( + extra.field_name.map(ToString::to_string), Some(field_type.to_string()), Some(value.clone().unbind()), ) .to_py_err()) } else { - self.fallback_warning(field_type, value); + self.fallback_warning(extra.field_name, field_type, value); Ok(()) } } @@ -408,14 +410,15 @@ impl CollectWarnings { // in particular, in future we could allow errors instead of warnings on fallback Err(S::Error::custom(UNEXPECTED_TYPE_SER_MARKER)) } else { - self.fallback_warning(field_type, value); + self.fallback_warning(extra.field_name, field_type, value); Ok(()) } } - fn fallback_warning(&self, field_type: &str, value: &Bound<'_, PyAny>) { + fn fallback_warning(&self, field_name: Option<&str>, field_type: &str, value: &Bound<'_, PyAny>) { if self.mode != WarningsMode::None { self.register_warning(PydanticSerializationUnexpectedValue::new_from_parts( + field_name.map(ToString::to_string), Some(field_type.to_string()), Some(value.clone().unbind()), )); diff --git a/src/serializers/fields.rs b/src/serializers/fields.rs index 6af2a0fc0..cf38eb40d 100644 --- a/src/serializers/fields.rs +++ b/src/serializers/fields.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::string::ToString; use std::sync::Arc; use pyo3::prelude::*; @@ -217,6 +218,7 @@ impl GeneralFieldsSerializer { } else if field_extra.check == SerCheck::Strict { return Err(PydanticSerializationUnexpectedValue::new( Some(format!("Unexpected field `{key}`")), + field_extra.field_name.map(ToString::to_string), field_extra.model_type_name().map(|bound| bound.to_string()), None, ) @@ -235,6 +237,7 @@ impl GeneralFieldsSerializer { Err(PydanticSerializationUnexpectedValue::new( Some(format!("Expected {required_fields} fields but got {used_req_fields}").to_string()), + extra.field_name.map(ToString::to_string), extra.model_type_name().map(|bound| bound.to_string()), extra.model.map(|bound| bound.clone().unbind()), ) diff --git a/src/serializers/type_serializers/union.rs b/src/serializers/type_serializers/union.rs index 2df50a694..529a4e27e 100644 --- a/src/serializers/type_serializers/union.rs +++ b/src/serializers/type_serializers/union.rs @@ -338,6 +338,7 @@ impl TaggedUnionSerializer { PydanticSerializationUnexpectedValue::new( Some("Defaulting to left to right union serialization - failed to get discriminator value for tagged union serialization".to_string()), None, + None, Some(value.clone().unbind()), ) ); diff --git a/tests/serializers/test_model.py b/tests/serializers/test_model.py index 963584c63..5926d6141 100644 --- a/tests/serializers/test_model.py +++ b/tests/serializers/test_model.py @@ -284,6 +284,12 @@ def test_model_wrong_warn(): ): assert s.to_python({'foo': 1, 'bar': b'more'}) == {'foo': 1, 'bar': b'more'} + with pytest.warns( + UserWarning, + match=r"Expected `int` - serialized value may not be as expected \[field_name=foo, input_value='lorem', input_type=str\]", + ): + assert s.to_python(BasicModel(foo='lorem')) == {'foo': 'lorem'} + def test_exclude_none(): s = SchemaSerializer(