Skip to content

Commit cd5cd65

Browse files
authored
Serialize support Path and add fallback function, JSON improvements (#514)
* serialize infer() support Path * add fallback function to to_json and to_python_jsonable * hardening json fallback logic * support fallback passing through ExtraOwned * fix test on windows
1 parent 330911a commit cd5cd65

File tree

11 files changed

+357
-98
lines changed

11 files changed

+357
-98
lines changed

pydantic_core/_pydantic_core.pyi

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import decimal
22
import sys
3-
from typing import Any
3+
from typing import Any, Callable
44

55
from pydantic_core import ErrorDetails, InitErrorDetails
66
from pydantic_core.core_schema import CoreConfig, CoreSchema, ErrorType
@@ -80,6 +80,7 @@ class SchemaSerializer:
8080
exclude_none: bool = False,
8181
round_trip: bool = False,
8282
warnings: bool = True,
83+
fallback: 'Callable[[Any], Any] | None' = None,
8384
) -> Any: ...
8485
def to_json(
8586
self,
@@ -94,6 +95,7 @@ class SchemaSerializer:
9495
exclude_none: bool = False,
9596
round_trip: bool = False,
9697
warnings: bool = True,
98+
fallback: 'Callable[[Any], Any] | None' = None,
9799
) -> bytes: ...
98100

99101
def to_json(
@@ -107,6 +109,7 @@ def to_json(
107109
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
108110
bytes_mode: Literal['utf8', 'base64'] = 'utf8',
109111
serialize_unknown: bool = False,
112+
fallback: 'Callable[[Any], Any] | None' = None,
110113
) -> bytes: ...
111114
def to_jsonable_python(
112115
value: Any,
@@ -118,6 +121,7 @@ def to_jsonable_python(
118121
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
119122
bytes_mode: Literal['utf8', 'base64'] = 'utf8',
120123
serialize_unknown: bool = False,
124+
fallback: 'Callable[[Any], Any] | None' = None,
121125
) -> Any: ...
122126

123127
class Url:

src/errors/validation_exception.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use serde::{Serialize, Serializer};
1313
use serde_json::ser::PrettyFormatter;
1414

1515
use crate::build_tools::{py_error_type, safe_repr, SchemaDict};
16-
use crate::serializers::GeneralSerializeContext;
16+
use crate::serializers::{SerMode, SerializationState};
1717
use crate::PydanticCustomError;
1818

1919
use super::line_error::ValLineError;
@@ -140,8 +140,8 @@ impl ValidationError {
140140
indent: Option<usize>,
141141
include_context: Option<bool>,
142142
) -> PyResult<&'py PyString> {
143-
let general_ser_context = GeneralSerializeContext::new();
144-
let extra = general_ser_context.extra(py, true);
143+
let state = SerializationState::new(None, None);
144+
let extra = state.extra(py, &SerMode::Json, None, None, Some(true), None);
145145
let serializer = ValidationErrorSerializer {
146146
py,
147147
line_errors: &self.line_errors,

src/serializers/errors.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use pyo3::prelude::*;
66
use serde::ser;
77

88
/// `UNEXPECTED_TYPE_SER` is a special prefix to denote a `PydanticSerializationUnexpectedValue` error.
9-
pub(super) static UNEXPECTED_TYPE_SER: &str = "__PydanticSerializationUnexpectedValue__";
9+
pub(super) static UNEXPECTED_TYPE_SER_MARKER: &str = "__PydanticSerializationUnexpectedValue__";
10+
pub(super) static SERIALIZATION_ERR_MARKER: &str = "__PydanticSerializationError__";
1011

1112
// convert a `PyErr` or `PyDowncastError` into a serde serialization error
1213
pub(super) fn py_err_se_err<T: ser::Error, E: fmt::Display>(py_error: E) -> T {
@@ -16,12 +17,14 @@ pub(super) fn py_err_se_err<T: ser::Error, E: fmt::Display>(py_error: E) -> T {
1617
/// convert a serde serialization error into a `PyErr`
1718
pub(super) fn se_err_py_err(error: serde_json::Error) -> PyErr {
1819
let s = error.to_string();
19-
if let Some(msg) = s.strip_prefix(UNEXPECTED_TYPE_SER) {
20+
if let Some(msg) = s.strip_prefix(UNEXPECTED_TYPE_SER_MARKER) {
2021
if msg.is_empty() {
2122
PydanticSerializationUnexpectedValue::new_err(None)
2223
} else {
2324
PydanticSerializationUnexpectedValue::new_err(Some(msg.to_string()))
2425
}
26+
} else if let Some(msg) = s.strip_prefix(SERIALIZATION_ERR_MARKER) {
27+
PydanticSerializationError::new_err(msg.to_string())
2528
} else {
2629
let msg = format!("Error serializing to JSON: {s}");
2730
PydanticSerializationError::new_err(msg)

src/serializers/extra.rs

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,62 @@ use pyo3::{intern, AsPyPointer};
88
use ahash::AHashSet;
99
use serde::ser::Error;
1010

11-
use crate::build_tools::py_err;
12-
1311
use super::config::SerializationConfig;
14-
use super::errors::{PydanticSerializationUnexpectedValue, UNEXPECTED_TYPE_SER};
12+
use super::errors::{PydanticSerializationUnexpectedValue, UNEXPECTED_TYPE_SER_MARKER};
1513
use super::ob_type::ObTypeLookup;
1614
use super::shared::CombinedSerializer;
1715

16+
/// this is ugly, would be much better if extra could be stored in `SerializationState`
17+
/// then `SerializationState` got a `serialize_infer` method, but I couldn't get it to work
18+
pub(crate) struct SerializationState {
19+
warnings: CollectWarnings,
20+
rec_guard: SerRecursionGuard,
21+
config: SerializationConfig,
22+
}
23+
24+
impl SerializationState {
25+
pub fn new(timedelta_mode: Option<&str>, bytes_mode: Option<&str>) -> Self {
26+
let warnings = CollectWarnings::new(None);
27+
let rec_guard = SerRecursionGuard::default();
28+
let config = SerializationConfig::from_args(timedelta_mode, bytes_mode).unwrap();
29+
Self {
30+
warnings,
31+
rec_guard,
32+
config,
33+
}
34+
}
35+
36+
pub fn extra<'py>(
37+
&'py self,
38+
py: Python<'py>,
39+
mode: &'py SerMode,
40+
exclude_none: Option<bool>,
41+
round_trip: Option<bool>,
42+
serialize_unknown: Option<bool>,
43+
fallback: Option<&'py PyAny>,
44+
) -> Extra<'py> {
45+
Extra::new(
46+
py,
47+
mode,
48+
&[],
49+
None,
50+
&self.warnings,
51+
None,
52+
None,
53+
exclude_none,
54+
round_trip,
55+
&self.config,
56+
&self.rec_guard,
57+
serialize_unknown,
58+
fallback,
59+
)
60+
}
61+
62+
pub fn final_check(&self, py: Python) -> PyResult<()> {
63+
self.warnings.final_check(py)
64+
}
65+
}
66+
1867
/// Useful things which are passed around by type_serializers
1968
#[derive(Clone)]
2069
#[cfg_attr(debug_assertions, derive(Debug))]
@@ -38,6 +87,7 @@ pub(crate) struct Extra<'a> {
3887
pub model: Option<&'a PyAny>,
3988
pub field_name: Option<&'a str>,
4089
pub serialize_unknown: bool,
90+
pub fallback: Option<&'a PyAny>,
4191
}
4292

4393
impl<'a> Extra<'a> {
@@ -55,6 +105,7 @@ impl<'a> Extra<'a> {
55105
config: &'a SerializationConfig,
56106
rec_guard: &'a SerRecursionGuard,
57107
serialize_unknown: Option<bool>,
108+
fallback: Option<&'a PyAny>,
58109
) -> Self {
59110
Self {
60111
mode,
@@ -72,6 +123,7 @@ impl<'a> Extra<'a> {
72123
model: None,
73124
field_name: None,
74125
serialize_unknown: serialize_unknown.unwrap_or(false),
126+
fallback,
75127
}
76128
}
77129

@@ -111,9 +163,10 @@ pub(crate) struct ExtraOwned {
111163
config: SerializationConfig,
112164
rec_guard: SerRecursionGuard,
113165
check: SerCheck,
114-
model: Option<Py<PyAny>>,
166+
model: Option<PyObject>,
115167
field_name: Option<String>,
116168
serialize_unknown: bool,
169+
fallback: Option<PyObject>,
117170
}
118171

119172
impl ExtraOwned {
@@ -133,6 +186,7 @@ impl ExtraOwned {
133186
model: extra.model.map(|v| v.into()),
134187
field_name: extra.field_name.map(|v| v.to_string()),
135188
serialize_unknown: extra.serialize_unknown,
189+
fallback: extra.fallback.map(|v| v.into()),
136190
}
137191
}
138192

@@ -153,6 +207,7 @@ impl ExtraOwned {
153207
model: self.model.as_ref().map(|m| m.as_ref(py)),
154208
field_name: self.field_name.as_ref().map(|n| n.as_ref()),
155209
serialize_unknown: self.serialize_unknown,
210+
fallback: self.fallback.as_ref().map(|m| m.as_ref(py)),
156211
}
157212
}
158213
}
@@ -248,7 +303,7 @@ impl CollectWarnings {
248303
// note: I think this should never actually happen since we use `to_python(..., mode='json')` during
249304
// JSON serialisation to "try" union branches, but it's here for completeness/correctness
250305
// in particular, in future we could allow errors instead of warnings on fallback
251-
Err(S::Error::custom(UNEXPECTED_TYPE_SER))
306+
Err(S::Error::custom(UNEXPECTED_TYPE_SER_MARKER))
252307
} else {
253308
self.fallback_warning(field_type, value);
254309
Ok(())
@@ -315,9 +370,9 @@ impl SerRecursionGuard {
315370
let id = value.as_ptr() as usize;
316371
let mut info = self.info.borrow_mut();
317372
if !info.ids.insert(id) {
318-
py_err!(PyValueError; "Circular reference detected (id repeated)")
373+
Err(PyValueError::new_err("Circular reference detected (id repeated)"))
319374
} else if info.depth > Self::MAX_DEPTH {
320-
py_err!(PyValueError; "Circular reference detected (depth exceeded)")
375+
Err(PyValueError::new_err("Circular reference detected (depth exceeded)"))
321376
} else {
322377
info.depth += 1;
323378
Ok(id)

src/serializers/infer.rs

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ use pyo3::types::{
88
PyTime, PyTuple,
99
};
1010

11-
use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer};
11+
use serde::ser::{Error, Serialize, SerializeMap, SerializeSeq, Serializer};
1212

1313
use crate::build_tools::{py_err, safe_repr};
14+
use crate::serializers::errors::SERIALIZATION_ERR_MARKER;
1415
use crate::serializers::filter::SchemaFilter;
1516
use crate::url::{PyMultiHostUrl, PyUrl};
1617

@@ -179,12 +180,18 @@ pub(crate) fn infer_to_python_known(
179180
}
180181
PyList::new(py, items).into_py(py)
181182
}
183+
ObType::Path => value.str()?.into_py(py),
182184
ObType::Unknown => {
183-
return if extra.serialize_unknown {
184-
Ok(serialize_unknown(value).into_py(py))
185+
if let Some(fallback) = extra.fallback {
186+
let next_value = fallback.call1((value,))?;
187+
let next_result = infer_to_python(next_value, include, exclude, extra);
188+
extra.rec_guard.pop(value_id);
189+
return next_result;
190+
} else if extra.serialize_unknown {
191+
serialize_unknown(value).into_py(py)
185192
} else {
186-
Err(unknown_type_error(value))
187-
};
193+
return Err(unknown_type_error(value));
194+
}
188195
}
189196
},
190197
_ => match ob_type {
@@ -232,6 +239,16 @@ pub(crate) fn infer_to_python_known(
232239
);
233240
iter.into_py(py)
234241
}
242+
ObType::Unknown => {
243+
if let Some(fallback) = extra.fallback {
244+
let next_value = fallback.call1((value,))?;
245+
let next_result = infer_to_python(next_value, include, exclude, extra);
246+
extra.rec_guard.pop(value_id);
247+
return next_result;
248+
} else {
249+
value.into_py(py)
250+
}
251+
}
235252
_ => value.into_py(py),
236253
},
237254
};
@@ -432,11 +449,25 @@ pub(crate) fn infer_serialize_known<S: Serializer>(
432449
}
433450
seq.end()
434451
}
452+
ObType::Path => {
453+
let s = value.str().map_err(py_err_se_err)?.to_str().map_err(py_err_se_err)?;
454+
serializer.serialize_str(s)
455+
}
435456
ObType::Unknown => {
436-
if extra.serialize_unknown {
457+
if let Some(fallback) = extra.fallback {
458+
let next_value = fallback.call1((value,)).map_err(py_err_se_err)?;
459+
let next_result = infer_serialize(next_value, serializer, include, exclude, extra);
460+
extra.rec_guard.pop(value_id);
461+
return next_result;
462+
} else if extra.serialize_unknown {
437463
serializer.serialize_str(&serialize_unknown(value))
438464
} else {
439-
return Err(py_err_se_err(unknown_type_error(value)));
465+
let msg = format!(
466+
"{}Unable to serialize unknown type: {}",
467+
SERIALIZATION_ERR_MARKER,
468+
safe_repr(value)
469+
);
470+
return Err(S::Error::custom(msg));
440471
}
441472
}
442473
};
@@ -452,9 +483,9 @@ fn serialize_unknown(value: &PyAny) -> Cow<str> {
452483
if let Ok(s) = value.str() {
453484
s.to_string_lossy()
454485
} else if let Ok(name) = value.get_type().name() {
455-
format!("<{name} object cannot be serialized to JSON>").into()
486+
format!("<Unserializable {name} object>").into()
456487
} else {
457-
"<object cannot be serialized to JSON>".into()
488+
"<Unserializable object>".into()
458489
}
459490
}
460491

@@ -531,8 +562,14 @@ pub(crate) fn infer_json_key_known<'py>(ob_type: &ObType, key: &'py PyAny, extra
531562
let k = key.getattr(intern!(key.py(), "value"))?;
532563
infer_json_key(k, extra)
533564
}
565+
ObType::Path => Ok(key.str()?.to_string_lossy()),
534566
ObType::Unknown => {
535-
if extra.serialize_unknown {
567+
if let Some(fallback) = extra.fallback {
568+
let next_key = fallback.call1((key,))?;
569+
// totally unnecessary step to placate rust's lifetime rules
570+
let next_key = next_key.to_object(key.py()).into_ref(key.py());
571+
infer_json_key(next_key, extra)
572+
} else if extra.serialize_unknown {
536573
Ok(serialize_unknown(key))
537574
} else {
538575
Err(unknown_type_error(key))

0 commit comments

Comments
 (0)