Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion python/pydantic_core/_pydantic_core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ class SchemaSerializer:
warnings: bool | Literal['none', 'warn', 'error'] = True,
fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
polymorphic_serialization: bool | None = None,
context: Any | None = None,
) -> Any:
"""
Expand All @@ -345,6 +346,7 @@ class SchemaSerializer:
fallback: A function to call when an unknown value is encountered,
if `None` a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.
polymorphic_serialization: Whether to override configured model and dataclass polymorphic serialization for this call.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should follow the existing pattern for the description? (That is, other parameters don't mention this "override" behavior of the configuration. Maybe in the actual docstring of the method, mention that these parameters override the corresponding configuration or similar).

context: The context to use for serialization, this is passed to functional serializers as
[`info.context`][pydantic_core.core_schema.SerializationInfo.context].

Expand All @@ -371,6 +373,7 @@ class SchemaSerializer:
warnings: bool | Literal['none', 'warn', 'error'] = True,
fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
polymorphic_serialization: bool | None = None,
context: Any | None = None,
) -> bytes:
"""
Expand All @@ -394,7 +397,9 @@ class SchemaSerializer:
"error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
fallback: A function to call when an unknown value is encountered,
if `None` a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.
serialize_as_any: Whether to serialize fields with duck-typing
serialization behavior.
polymorphic_serialization: Whether to override configured model and dataclass polymorphic serialization for this call.
context: The context to use for serialization, this is passed to functional serializers as
[`info.context`][pydantic_core.core_schema.SerializationInfo.context].

Expand Down Expand Up @@ -425,6 +430,7 @@ def to_json(
serialize_unknown: bool = False,
fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
polymorphic_serialization: bool | None = None,
context: Any | None = None,
) -> bytes:
"""
Expand Down Expand Up @@ -453,6 +459,7 @@ def to_json(
fallback: A function to call when an unknown value is encountered,
if `None` a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.
polymorphic_serialization: Whether to override configured model and dataclass polymorphic serialization for this call.
context: The context to use for serialization, this is passed to functional serializers as
[`info.context`][pydantic_core.core_schema.SerializationInfo.context].

Expand Down Expand Up @@ -510,6 +517,7 @@ def to_jsonable_python(
serialize_unknown: bool = False,
fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
polymorphic_serialization: bool | None = None,
context: Any | None = None,
) -> Any:
"""
Expand All @@ -536,6 +544,7 @@ def to_jsonable_python(
fallback: A function to call when an unknown value is encountered,
if `None` a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.
polymorphic_serialization: Whether to override configured model and dataclass polymorphic serialization for this call.
context: The context to use for serialization, this is passed to functional serializers as
[`info.context`][pydantic_core.core_schema.SerializationInfo.context].

Expand Down
7 changes: 7 additions & 0 deletions python/pydantic_core/core_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class CoreConfig(TypedDict, total=False):
validate_by_alias: Whether to use the field's alias when validating against the provided input data. Default is `True`.
validate_by_name: Whether to use the field's name when validating against the provided input data. Default is `False`. Replacement for `populate_by_name`.
serialize_by_alias: Whether to serialize by alias. Default is `False`, expected to change to `True` in V3.
polymorphic_serialization: Whether to enable polymorphic serialization for models and dataclasses. Default is `False`.
url_preserve_empty_path: Whether to preserve empty URL paths when validating values for a URL type. Defaults to `False`.
"""

Expand Down Expand Up @@ -120,6 +121,7 @@ class CoreConfig(TypedDict, total=False):
validate_by_alias: bool # default: True
validate_by_name: bool # default: False
serialize_by_alias: bool # default: False
polymorphic_serialization: bool # default: False
url_preserve_empty_path: bool # default: False


Expand Down Expand Up @@ -181,6 +183,11 @@ def serialize_as_any(self) -> bool:
"""The `serialize_as_any` argument set during serialization."""
...

@property
def polymorphic_serialization(self) -> bool | None:
"""The `polymorphic_serialization` argument set during serialization, if any."""
...

@property
def round_trip(self) -> bool:
"""The `round_trip` argument set during serialization."""
Expand Down
1 change: 1 addition & 0 deletions src/errors/validation_exception.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ impl ValidationError {
None,
false,
None,
None,
);
let mut state = SerializationState::new(config, WarningsMode::None, None, None, extra)?;
let mut serializer = ValidationErrorSerializer {
Expand Down
7 changes: 7 additions & 0 deletions src/serializers/extra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ pub(crate) struct Extra<'a, 'py> {
pub serialize_unknown: bool,
pub fallback: Option<&'a Bound<'py, PyAny>>,
pub serialize_as_any: bool,
/// Whether `polymorphic_serialization` is globally enabled / disabled for this serialization process
pub polymorphic_serialization: Option<bool>,
pub context: Option<&'a Bound<'py, PyAny>>,
}

Expand All @@ -197,6 +199,7 @@ impl<'a, 'py> Extra<'a, 'py> {
serialize_unknown: bool,
fallback: Option<&'a Bound<'py, PyAny>>,
serialize_as_any: bool,
polymorphic_serialization: Option<bool>,
context: Option<&'a Bound<'py, PyAny>>,
) -> Self {
Self {
Expand All @@ -211,6 +214,7 @@ impl<'a, 'py> Extra<'a, 'py> {
serialize_unknown,
fallback,
serialize_as_any,
polymorphic_serialization,
context,
}
}
Expand Down Expand Up @@ -256,6 +260,7 @@ pub(crate) struct ExtraOwned {
serialize_unknown: bool,
pub fallback: Option<Py<PyAny>>,
serialize_as_any: bool,
polymorphic_serialization: Option<bool>,
pub context: Option<Py<PyAny>>,
include: Option<Py<PyAny>>,
exclude: Option<Py<PyAny>>,
Expand Down Expand Up @@ -299,6 +304,7 @@ impl ExtraOwned {
serialize_unknown: extra.serialize_unknown,
fallback: extra.fallback.map(|model| model.clone().into()),
serialize_as_any: extra.serialize_as_any,
polymorphic_serialization: extra.polymorphic_serialization,
context: extra.context.map(|model| model.clone().into()),
include: state.include().map(|m| m.clone().into()),
exclude: state.exclude().map(|m| m.clone().into()),
Expand All @@ -318,6 +324,7 @@ impl ExtraOwned {
serialize_unknown: self.serialize_unknown,
fallback: self.fallback.as_ref().map(|m| m.bind(py)),
serialize_as_any: self.serialize_as_any,
polymorphic_serialization: self.polymorphic_serialization,
context: self.context.as_ref().map(|m| m.bind(py)),
}
}
Expand Down
19 changes: 15 additions & 4 deletions src/serializers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ mod fields;
mod filter;
mod infer;
mod ob_type;
mod polymorphism_trampoline;
mod prebuilt;
pub mod ser;
mod shared;
Expand Down Expand Up @@ -71,7 +72,8 @@ impl SchemaSerializer {
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (value, *, mode = None, include = None, exclude = None, by_alias = None,
exclude_unset = false, exclude_defaults = false, exclude_none = false, exclude_computed_fields = false,
round_trip = false, warnings = WarningsArg::Bool(true), fallback = None, serialize_as_any = false, context = None))]
round_trip = false, warnings = WarningsArg::Bool(true), fallback = None, serialize_as_any = false,
polymorphic_serialization = None, context = None))]
pub fn to_python(
&self,
py: Python,
Expand All @@ -88,6 +90,7 @@ impl SchemaSerializer {
warnings: WarningsArg,
fallback: Option<&Bound<'_, PyAny>>,
serialize_as_any: bool,
polymorphic_serialization: Option<bool>,
context: Option<&Bound<'_, PyAny>>,
) -> PyResult<Py<PyAny>> {
let mode: SerMode = mode.into();
Expand All @@ -107,6 +110,7 @@ impl SchemaSerializer {
false,
fallback,
serialize_as_any,
polymorphic_serialization,
context,
);
let mut state = SerializationState::new(self.config, warnings_mode, include, exclude, extra)?;
Expand All @@ -118,7 +122,8 @@ impl SchemaSerializer {
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (value, *, indent = None, ensure_ascii = false, include = None, exclude = None, by_alias = None,
exclude_unset = false, exclude_defaults = false, exclude_none = false, exclude_computed_fields = false,
round_trip = false, warnings = WarningsArg::Bool(true), fallback = None, serialize_as_any = false, context = None))]
round_trip = false, warnings = WarningsArg::Bool(true), fallback = None, serialize_as_any = false,
polymorphic_serialization = None, context = None))]
pub fn to_json(
&self,
py: Python,
Expand All @@ -136,6 +141,7 @@ impl SchemaSerializer {
warnings: WarningsArg,
fallback: Option<&Bound<'_, PyAny>>,
serialize_as_any: bool,
polymorphic_serialization: Option<bool>,
context: Option<&Bound<'_, PyAny>>,
) -> PyResult<Py<PyAny>> {
let warnings_mode = match warnings {
Expand All @@ -154,6 +160,7 @@ impl SchemaSerializer {
false,
fallback,
serialize_as_any,
polymorphic_serialization,
context,
);
let mut state = SerializationState::new(self.config, warnings_mode, include, exclude, extra)?;
Expand Down Expand Up @@ -201,7 +208,7 @@ impl SchemaSerializer {
#[pyo3(signature = (value, *, indent = None, ensure_ascii = false, include = None, exclude = None, by_alias = true,
exclude_none = false, round_trip = false, timedelta_mode = "iso8601", temporal_mode = "iso8601",
bytes_mode = "utf8", inf_nan_mode = "constants", serialize_unknown = false, fallback = None,
serialize_as_any = false, context = None))]
serialize_as_any = false, polymorphic_serialization = None, context = None))]
pub fn to_json(
py: Python,
value: &Bound<'_, PyAny>,
Expand All @@ -219,6 +226,7 @@ pub fn to_json(
serialize_unknown: bool,
fallback: Option<&Bound<'_, PyAny>>,
serialize_as_any: bool,
polymorphic_serialization: Option<bool>,
context: Option<&Bound<'_, PyAny>>,
) -> PyResult<Py<PyAny>> {
let config = SerializationConfig::from_args(timedelta_mode, temporal_mode, bytes_mode, inf_nan_mode)?;
Expand All @@ -234,6 +242,7 @@ pub fn to_json(
serialize_unknown,
fallback,
serialize_as_any,
polymorphic_serialization,
context,
);
let mut state = SerializationState::new(config, WarningsMode::None, include, exclude, extra)?;
Expand All @@ -254,7 +263,7 @@ pub fn to_json(
#[pyfunction]
#[pyo3(signature = (value, *, include = None, exclude = None, by_alias = true, exclude_none = false, round_trip = false,
timedelta_mode = "iso8601", temporal_mode = "iso8601", bytes_mode = "utf8", inf_nan_mode = "constants",
serialize_unknown = false, fallback = None, serialize_as_any = false, context = None))]
serialize_unknown = false, fallback = None, serialize_as_any = false, polymorphic_serialization = None, context = None))]
pub fn to_jsonable_python(
py: Python,
value: &Bound<'_, PyAny>,
Expand All @@ -270,6 +279,7 @@ pub fn to_jsonable_python(
serialize_unknown: bool,
fallback: Option<&Bound<'_, PyAny>>,
serialize_as_any: bool,
polymorphic_serialization: Option<bool>,
context: Option<&Bound<'_, PyAny>>,
) -> PyResult<Py<PyAny>> {
let config = SerializationConfig::from_args(timedelta_mode, temporal_mode, bytes_mode, inf_nan_mode)?;
Expand All @@ -285,6 +295,7 @@ pub fn to_jsonable_python(
serialize_unknown,
fallback,
serialize_as_any,
polymorphic_serialization,
context,
);
let mut state = SerializationState::new(config, WarningsMode::None, include, exclude, extra)?;
Expand Down
96 changes: 96 additions & 0 deletions src/serializers/polymorphism_trampoline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use std::{borrow::Cow, sync::Arc};

use pyo3::{prelude::*, types::PyType};

use crate::serializers::{
errors::unwrap_ser_error,
extra::SerCheck,
infer::call_pydantic_serializer,
shared::{serialize_to_json, serialize_to_python, DoSerialize, TypeSerializer},
CombinedSerializer, SerializationState,
};

/// The polymorphism trampoline detects subclasses of its target type and dispatches to their
/// `__pydantic_serializer__` serializer for serialization.
///
/// This exists as a separate structure to allow for cases such as model serializers where the
/// inner serializer may just be a function serializer and so cannot handle polymorphism itself.
#[derive(Debug)]
pub struct PolymorphismTrampoline {
class: Py<PyType>,
/// Inner serializer used when the type is not a subclass (responsible for any fallback etc)
pub(crate) serializer: Arc<CombinedSerializer>,
/// Whether polymorphic serialization is enabled from config
enabled_from_config: bool,
}

impl_py_gc_traverse!(PolymorphismTrampoline { class, serializer });

impl PolymorphismTrampoline {
pub fn new(class: Py<PyType>, serializer: Arc<CombinedSerializer>, enabled_from_config: bool) -> Self {
Self {
class,
serializer,
enabled_from_config,
}
}

fn is_subclass(&self, value: &Bound<'_, PyAny>) -> PyResult<bool> {
Ok(!value.get_type().is(&self.class) && value.is_instance(self.class.bind(value.py()))?)
}

fn serialize<'py, T, E: From<PyErr>>(
&self,
value: &Bound<'py, PyAny>,
state: &mut SerializationState<'_, 'py>,
do_serialize: impl DoSerialize<'py, T, E>,
) -> Result<T, E> {
let runtime_polymorphic = state.extra.polymorphic_serialization;
if state.check != SerCheck::Strict // strict disables polymorphism
&& runtime_polymorphic.unwrap_or(self.enabled_from_config)
&& self.is_subclass(value)?
{
call_pydantic_serializer(value, state, do_serialize)
} else {
do_serialize.serialize_no_infer(&self.serializer, value, state)
}
}
}

impl TypeSerializer for PolymorphismTrampoline {
fn to_python<'py>(
&self,
value: &Bound<'py, PyAny>,
state: &mut SerializationState<'_, 'py>,
) -> PyResult<Py<PyAny>> {
self.serialize(value, state, serialize_to_python())
}

fn json_key<'a, 'py>(
&self,
key: &'a Bound<'py, PyAny>,
state: &mut SerializationState<'_, 'py>,
) -> PyResult<Cow<'a, str>> {
// json key serialization for models and dataclasses was always polymorphic anyway
// FIXME: make this consistent with the other cases?
self.serializer.json_key(key, state)
}

fn serde_serialize<'py, S: serde::ser::Serializer>(
&self,
value: &Bound<'py, PyAny>,
serializer: S,
state: &mut SerializationState<'_, 'py>,
) -> Result<S::Ok, S::Error> {
self.serialize(value, state, serialize_to_json(serializer))
.map_err(unwrap_ser_error)
}

fn get_name(&self) -> &str {
self.serializer.get_name()
}

fn retry_with_lax_check(&self) -> bool {
self.serializer.retry_with_lax_check()
}
}
22 changes: 17 additions & 5 deletions src/serializers/prebuilt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ use std::borrow::Cow;
use pyo3::prelude::*;
use pyo3::types::PyDict;

use crate::common::prebuilt::get_prebuilt;
use crate::serializers::SerializationState;
use crate::SchemaSerializer;
use crate::{common::prebuilt::get_prebuilt, serializers::polymorphism_trampoline::PolymorphismTrampoline};

use super::shared::{CombinedSerializer, TypeSerializer};

Expand All @@ -18,12 +18,24 @@ impl PrebuiltSerializer {
pub fn try_get_from_schema(type_: &str, schema: &Bound<'_, PyDict>) -> PyResult<Option<CombinedSerializer>> {
get_prebuilt(type_, schema, "__pydantic_serializer__", |py_any| {
let schema_serializer = py_any.extract::<Py<SchemaSerializer>>()?;
if matches!(
schema_serializer.get().serializer.as_ref(),
CombinedSerializer::FunctionWrap(_)
) {

let mut serializer = schema_serializer.get().serializer.as_ref();

// it is very likely that the prebuilt serializer is a polymorphism trampoline, peek
// through it for the sake of the check below
if let CombinedSerializer::PolymorphismTrampoline(PolymorphismTrampoline {
serializer: inner_serializer,
..
}) = serializer
{
serializer = inner_serializer.as_ref();
}

// don't allow wrap serializers as prebuilt serializers (leads to double wrapping)
if matches!(serializer, CombinedSerializer::FunctionWrap(_)) {
return Ok(None);
}

Ok(Some(Self { schema_serializer }.into()))
})
}
Expand Down
Loading
Loading