Skip to content

Commit 4a0c332

Browse files
Clean up serialization warnings for invalid data in unions (#1449)
1 parent 16331d7 commit 4a0c332

File tree

4 files changed

+51
-80
lines changed

4 files changed

+51
-80
lines changed

src/serializers/extra.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,15 @@ impl CollectWarnings {
392392
if value.is_none() {
393393
Ok(())
394394
} else if extra.check.enabled() {
395-
Err(PydanticSerializationUnexpectedValue::new_err(None))
395+
let type_name = value
396+
.get_type()
397+
.qualname()
398+
.unwrap_or_else(|_| PyString::new_bound(value.py(), "<unknown python object>"));
399+
400+
let value_str = truncate_safe_repr(value, None);
401+
Err(PydanticSerializationUnexpectedValue::new_err(Some(format!(
402+
"Expected `{field_type}` but got `{type_name}` with value `{value_str}` - serialized value may not be as expected"
403+
))))
396404
} else {
397405
self.fallback_warning(field_type, value);
398406
Ok(())

src/serializers/type_serializers/union.rs

Lines changed: 29 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ use std::borrow::Cow;
88
use crate::build_tools::py_schema_err;
99
use crate::common::union::{Discriminator, SMALL_UNION_THRESHOLD};
1010
use crate::definitions::DefinitionsBuilder;
11-
use crate::serializers::type_serializers::py_err_se_err;
1211
use crate::tools::{truncate_safe_repr, SchemaDict};
13-
use crate::PydanticSerializationUnexpectedValue;
1412

1513
use super::{
1614
infer_json_key, infer_serialize, infer_to_python, BuildSerializer, CombinedSerializer, Extra, SerCheck,
@@ -77,7 +75,6 @@ fn to_python(
7775
exclude: Option<&Bound<'_, PyAny>>,
7876
extra: &Extra,
7977
choices: &[CombinedSerializer],
80-
name: &str,
8178
retry_with_lax_check: bool,
8279
) -> PyResult<PyObject> {
8380
// try the serializers in left to right order with error_on fallback=true
@@ -88,22 +85,15 @@ fn to_python(
8885
for comb_serializer in choices {
8986
match comb_serializer.to_python(value, include, exclude, &new_extra) {
9087
Ok(v) => return Ok(v),
91-
Err(err) => match err.is_instance_of::<PydanticSerializationUnexpectedValue>(value.py()) {
92-
true => (),
93-
false => errors.push(err),
94-
},
88+
Err(err) => errors.push(err),
9589
}
9690
}
9791

9892
if retry_with_lax_check {
9993
new_extra.check = SerCheck::Lax;
10094
for comb_serializer in choices {
101-
match comb_serializer.to_python(value, include, exclude, &new_extra) {
102-
Ok(v) => return Ok(v),
103-
Err(err) => match err.is_instance_of::<PydanticSerializationUnexpectedValue>(value.py()) {
104-
true => (),
105-
false => errors.push(err),
106-
},
95+
if let Ok(v) = comb_serializer.to_python(value, include, exclude, &new_extra) {
96+
return Ok(v);
10797
}
10898
}
10999
}
@@ -112,15 +102,13 @@ fn to_python(
112102
extra.warnings.custom_warning(err.to_string());
113103
}
114104

115-
extra.warnings.on_fallback_py(name, value, extra)?;
116105
infer_to_python(value, include, exclude, extra)
117106
}
118107

119108
fn json_key<'a>(
120109
key: &'a Bound<'_, PyAny>,
121110
extra: &Extra,
122111
choices: &[CombinedSerializer],
123-
name: &str,
124112
retry_with_lax_check: bool,
125113
) -> PyResult<Cow<'a, str>> {
126114
let mut new_extra = extra.clone();
@@ -130,22 +118,15 @@ fn json_key<'a>(
130118
for comb_serializer in choices {
131119
match comb_serializer.json_key(key, &new_extra) {
132120
Ok(v) => return Ok(v),
133-
Err(err) => match err.is_instance_of::<PydanticSerializationUnexpectedValue>(key.py()) {
134-
true => (),
135-
false => errors.push(err),
136-
},
121+
Err(err) => errors.push(err),
137122
}
138123
}
139124

140125
if retry_with_lax_check {
141126
new_extra.check = SerCheck::Lax;
142127
for comb_serializer in choices {
143-
match comb_serializer.json_key(key, &new_extra) {
144-
Ok(v) => return Ok(v),
145-
Err(err) => match err.is_instance_of::<PydanticSerializationUnexpectedValue>(key.py()) {
146-
true => (),
147-
false => errors.push(err),
148-
},
128+
if let Ok(v) = comb_serializer.json_key(key, &new_extra) {
129+
return Ok(v);
149130
}
150131
}
151132
}
@@ -154,7 +135,6 @@ fn json_key<'a>(
154135
extra.warnings.custom_warning(err.to_string());
155136
}
156137

157-
extra.warnings.on_fallback_py(name, key, extra)?;
158138
infer_json_key(key, extra)
159139
}
160140

@@ -166,7 +146,6 @@ fn serde_serialize<S: serde::ser::Serializer>(
166146
exclude: Option<&Bound<'_, PyAny>>,
167147
extra: &Extra,
168148
choices: &[CombinedSerializer],
169-
name: &str,
170149
retry_with_lax_check: bool,
171150
) -> Result<S::Ok, S::Error> {
172151
let py = value.py();
@@ -177,22 +156,15 @@ fn serde_serialize<S: serde::ser::Serializer>(
177156
for comb_serializer in choices {
178157
match comb_serializer.to_python(value, include, exclude, &new_extra) {
179158
Ok(v) => return infer_serialize(v.bind(py), serializer, None, None, extra),
180-
Err(err) => match err.is_instance_of::<PydanticSerializationUnexpectedValue>(py) {
181-
true => (),
182-
false => errors.push(err),
183-
},
159+
Err(err) => errors.push(err),
184160
}
185161
}
186162

187163
if retry_with_lax_check {
188164
new_extra.check = SerCheck::Lax;
189165
for comb_serializer in choices {
190-
match comb_serializer.to_python(value, include, exclude, &new_extra) {
191-
Ok(v) => return infer_serialize(v.bind(py), serializer, None, None, extra),
192-
Err(err) => match err.is_instance_of::<PydanticSerializationUnexpectedValue>(py) {
193-
true => (),
194-
false => errors.push(err),
195-
},
166+
if let Ok(v) = comb_serializer.to_python(value, include, exclude, &new_extra) {
167+
return infer_serialize(v.bind(py), serializer, None, None, extra);
196168
}
197169
}
198170
}
@@ -201,7 +173,6 @@ fn serde_serialize<S: serde::ser::Serializer>(
201173
extra.warnings.custom_warning(err.to_string());
202174
}
203175

204-
extra.warnings.on_fallback_ser::<S>(name, value, extra)?;
205176
infer_serialize(value, serializer, include, exclude, extra)
206177
}
207178

@@ -219,13 +190,12 @@ impl TypeSerializer for UnionSerializer {
219190
exclude,
220191
extra,
221192
&self.choices,
222-
self.get_name(),
223193
self.retry_with_lax_check(),
224194
)
225195
}
226196

227197
fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult<Cow<'a, str>> {
228-
json_key(key, extra, &self.choices, self.get_name(), self.retry_with_lax_check())
198+
json_key(key, extra, &self.choices, self.retry_with_lax_check())
229199
}
230200

231201
fn serde_serialize<S: serde::ser::Serializer>(
@@ -243,7 +213,6 @@ impl TypeSerializer for UnionSerializer {
243213
exclude,
244214
extra,
245215
&self.choices,
246-
self.get_name(),
247216
self.retry_with_lax_check(),
248217
)
249218
}
@@ -313,8 +282,6 @@ impl TypeSerializer for TaggedUnionSerializer {
313282
exclude: Option<&Bound<'_, PyAny>>,
314283
extra: &Extra,
315284
) -> PyResult<PyObject> {
316-
let py = value.py();
317-
318285
let mut new_extra = extra.clone();
319286
new_extra.check = SerCheck::Strict;
320287

@@ -325,15 +292,14 @@ impl TypeSerializer for TaggedUnionSerializer {
325292

326293
match serializer.to_python(value, include, exclude, &new_extra) {
327294
Ok(v) => return Ok(v),
328-
Err(err) => match err.is_instance_of::<PydanticSerializationUnexpectedValue>(py) {
329-
true => {
330-
if self.retry_with_lax_check() {
331-
new_extra.check = SerCheck::Lax;
332-
return serializer.to_python(value, include, exclude, &new_extra);
295+
Err(_) => {
296+
if self.retry_with_lax_check() {
297+
new_extra.check = SerCheck::Lax;
298+
if let Ok(v) = serializer.to_python(value, include, exclude, &new_extra) {
299+
return Ok(v);
333300
}
334301
}
335-
false => return Err(err),
336-
},
302+
}
337303
}
338304
}
339305
}
@@ -344,13 +310,11 @@ impl TypeSerializer for TaggedUnionSerializer {
344310
exclude,
345311
extra,
346312
&self.choices,
347-
self.get_name(),
348313
self.retry_with_lax_check(),
349314
)
350315
}
351316

352317
fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult<Cow<'a, str>> {
353-
let py = key.py();
354318
let mut new_extra = extra.clone();
355319
new_extra.check = SerCheck::Strict;
356320

@@ -361,20 +325,19 @@ impl TypeSerializer for TaggedUnionSerializer {
361325

362326
match serializer.json_key(key, &new_extra) {
363327
Ok(v) => return Ok(v),
364-
Err(err) => match err.is_instance_of::<PydanticSerializationUnexpectedValue>(py) {
365-
true => {
366-
if self.retry_with_lax_check() {
367-
new_extra.check = SerCheck::Lax;
368-
return serializer.json_key(key, &new_extra);
328+
Err(_) => {
329+
if self.retry_with_lax_check() {
330+
new_extra.check = SerCheck::Lax;
331+
if let Ok(v) = serializer.json_key(key, &new_extra) {
332+
return Ok(v);
369333
}
370334
}
371-
false => return Err(err),
372-
},
335+
}
373336
}
374337
}
375338
}
376339

377-
json_key(key, extra, &self.choices, self.get_name(), self.retry_with_lax_check())
340+
json_key(key, extra, &self.choices, self.retry_with_lax_check())
378341
}
379342

380343
fn serde_serialize<S: serde::ser::Serializer>(
@@ -396,18 +359,14 @@ impl TypeSerializer for TaggedUnionSerializer {
396359

397360
match selected_serializer.to_python(value, include, exclude, &new_extra) {
398361
Ok(v) => return infer_serialize(v.bind(py), serializer, None, None, extra),
399-
Err(err) => match err.is_instance_of::<PydanticSerializationUnexpectedValue>(py) {
400-
true => {
401-
if self.retry_with_lax_check() {
402-
new_extra.check = SerCheck::Lax;
403-
match selected_serializer.to_python(value, include, exclude, &new_extra) {
404-
Ok(v) => return infer_serialize(v.bind(py), serializer, None, None, extra),
405-
Err(err) => return Err(py_err_se_err(err)),
406-
}
362+
Err(_) => {
363+
if self.retry_with_lax_check() {
364+
new_extra.check = SerCheck::Lax;
365+
if let Ok(v) = selected_serializer.to_python(value, include, exclude, &new_extra) {
366+
return infer_serialize(v.bind(py), serializer, None, None, extra);
407367
}
408368
}
409-
false => return Err(py_err_se_err(err)),
410-
},
369+
}
411370
}
412371
}
413372
}
@@ -419,7 +378,6 @@ impl TypeSerializer for TaggedUnionSerializer {
419378
exclude,
420379
extra,
421380
&self.choices,
422-
self.get_name(),
423381
self.retry_with_lax_check(),
424382
)
425383
}

tests/serializers/test_model.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
from pydantic_core import (
1717
PydanticSerializationError,
18-
PydanticSerializationUnexpectedValue,
1918
SchemaSerializer,
2019
SchemaValidator,
2120
core_schema,
@@ -1150,8 +1149,6 @@ class BModel(BasicModel): ...
11501149
)
11511150
)
11521151

1153-
with pytest.raises(
1154-
PydanticSerializationUnexpectedValue, match='Expected 2 fields but got 1 for type `.*AModel` with value `.*`.+'
1155-
):
1152+
with pytest.warns(UserWarning, match='Expected 2 fields but got 1 for type `.*AModel` with value `.*`.+'):
11561153
value = BasicModel(root=AModel(type='a'))
11571154
s.to_python(value)

tests/serializers/test_union.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import dataclasses
22
import json
3-
import re
43
import uuid
4+
import warnings
55
from decimal import Decimal
66
from typing import Any, ClassVar, Union
77

@@ -32,9 +32,17 @@ def test_union_bool_int(input_value, expected_value, bool_case_label, int_case_l
3232

3333
def test_union_error():
3434
s = SchemaSerializer(core_schema.union_schema([core_schema.bool_schema(), core_schema.int_schema()]))
35-
msg = "Expected `Union[bool, int]` but got `str` with value `'a string'` - serialized value may not be as expected"
36-
with pytest.warns(UserWarning, match=re.escape(msg)):
37-
assert s.to_python('a string') == 'a string'
35+
36+
messages = [
37+
"Expected `bool` but got `str` with value `'a string'` - serialized value may not be as expected",
38+
"Expected `int` but got `str` with value `'a string'` - serialized value may not be as expected",
39+
]
40+
41+
with warnings.catch_warnings(record=True) as w:
42+
warnings.simplefilter('always')
43+
s.to_python('a string')
44+
for m in messages:
45+
assert m in str(w[0].message)
3846

3947

4048
class ModelA:

0 commit comments

Comments
 (0)