diff --git a/src/common/prebuilt.rs b/src/common/prebuilt.rs index f4307064f..e66e2c35b 100644 --- a/src/common/prebuilt.rs +++ b/src/common/prebuilt.rs @@ -8,7 +8,7 @@ pub fn get_prebuilt( type_: &str, schema: &Bound<'_, PyDict>, prebuilt_attr_name: &str, - extractor: impl FnOnce(Bound<'_, PyAny>) -> PyResult, + extractor: impl FnOnce(Bound<'_, PyAny>) -> PyResult>, ) -> PyResult> { let py = schema.py(); @@ -40,5 +40,5 @@ pub fn get_prebuilt( // Retrieve the prebuilt validator / serializer if available let prebuilt: Bound<'_, PyAny> = class_dict.get_item(prebuilt_attr_name)?; - extractor(prebuilt).map(Some) + extractor(prebuilt) } diff --git a/src/serializers/prebuilt.rs b/src/serializers/prebuilt.rs index 33d197d9b..3f1cf1c68 100644 --- a/src/serializers/prebuilt.rs +++ b/src/serializers/prebuilt.rs @@ -17,9 +17,11 @@ pub struct PrebuiltSerializer { impl PrebuiltSerializer { pub fn try_get_from_schema(type_: &str, schema: &Bound<'_, PyDict>) -> PyResult> { get_prebuilt(type_, schema, "__pydantic_serializer__", |py_any| { - py_any - .extract::>() - .map(|schema_serializer| Self { schema_serializer }.into()) + let schema_serializer = py_any.extract::>()?; + if matches!(schema_serializer.get().serializer, CombinedSerializer::FunctionWrap(_)) { + return Ok(None); + } + Ok(Some(Self { schema_serializer }.into())) }) } } diff --git a/src/validators/prebuilt.rs b/src/validators/prebuilt.rs index c17acb9f9..54c40f197 100644 --- a/src/validators/prebuilt.rs +++ b/src/validators/prebuilt.rs @@ -16,9 +16,11 @@ pub struct PrebuiltValidator { impl PrebuiltValidator { pub fn try_get_from_schema(type_: &str, schema: &Bound<'_, PyDict>) -> PyResult> { get_prebuilt(type_, schema, "__pydantic_validator__", |py_any| { - py_any - .extract::>() - .map(|schema_validator| Self { schema_validator }.into()) + let schema_validator = py_any.extract::>()?; + if matches!(schema_validator.get().validator, CombinedValidator::FunctionWrap(_)) { + return Ok(None); + } + Ok(Some(Self { schema_validator }.into())) }) } } diff --git a/tests/test_prebuilt.py b/tests/test_prebuilt.py index 9cd5aa325..14a9f288b 100644 --- a/tests/test_prebuilt.py +++ b/tests/test_prebuilt.py @@ -1,3 +1,5 @@ +from typing import Union + from pydantic_core import SchemaSerializer, SchemaValidator, core_schema @@ -46,3 +48,238 @@ class OuterModel: result = outer_validator.validate_python({'inner': {'x': 1}}) assert result.inner.x == 1 assert outer_serializer.to_python(result) == {'inner': {'x': 1}} + + +def test_prebuilt_not_used_for_wrap_serializer_functions() -> None: + class InnerModel: + x: str + + def __init__(self, x: str) -> None: + self.x = x + + def serialize_inner(v: InnerModel, serializer) -> Union[dict[str, str], str]: + v.x = v.x + ' modified' + return serializer(v) + + inner_schema = core_schema.model_schema( + InnerModel, + schema=core_schema.model_fields_schema( + {'x': core_schema.model_field(schema=core_schema.str_schema())}, + ), + serialization=core_schema.wrap_serializer_function_ser_schema(serialize_inner), + ) + + inner_schema_serializer = SchemaSerializer(inner_schema) + InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue] + InnerModel.__pydantic_serializer__ = inner_schema_serializer # pyright: ignore[reportAttributeAccessIssue] + + class OuterModel: + inner: InnerModel + + def __init__(self, inner: InnerModel) -> None: + self.inner = inner + + outer_schema = core_schema.model_schema( + OuterModel, + schema=core_schema.model_fields_schema( + { + 'inner': core_schema.model_field( + schema=core_schema.model_schema( + InnerModel, + schema=core_schema.model_fields_schema( + # note, we use a simple str schema (with no custom serialization) + # in order to verify that the prebuilt serializer from InnerModel is not used + {'x': core_schema.model_field(schema=core_schema.str_schema())}, + ), + ) + ) + } + ), + ) + + inner_serializer = SchemaSerializer(inner_schema) + outer_serializer = SchemaSerializer(outer_schema) + + # the custom serialization function does apply for the inner model + inner_instance = InnerModel(x='hello') + assert inner_serializer.to_python(inner_instance) == {'x': 'hello modified'} + + # but the outer model doesn't reuse the custom wrap serializer function, so we see simple str ser + outer_instance = OuterModel(inner=InnerModel(x='hello')) + assert outer_serializer.to_python(outer_instance) == {'inner': {'x': 'hello'}} + + +def test_prebuilt_not_used_for_wrap_validator_functions() -> None: + class InnerModel: + x: str + + def __init__(self, x: str) -> None: + self.x = x + + def validate_inner(data, validator) -> InnerModel: + data['x'] = data['x'] + ' modified' + return validator(data) + + inner_schema = core_schema.no_info_wrap_validator_function( + validate_inner, + core_schema.model_schema( + InnerModel, + schema=core_schema.model_fields_schema( + {'x': core_schema.model_field(schema=core_schema.str_schema())}, + ), + ), + ) + + inner_schema_validator = SchemaValidator(inner_schema) + InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue] + InnerModel.__pydantic_validator__ = inner_schema_validator # pyright: ignore[reportAttributeAccessIssue] + + class OuterModel: + inner: InnerModel + + def __init__(self, inner: InnerModel) -> None: + self.inner = inner + + outer_schema = core_schema.model_schema( + OuterModel, + schema=core_schema.model_fields_schema( + { + 'inner': core_schema.model_field( + schema=core_schema.model_schema( + InnerModel, + schema=core_schema.model_fields_schema( + # note, we use a simple str schema (with no custom validation) + # in order to verify that the prebuilt validator from InnerModel is not used + {'x': core_schema.model_field(schema=core_schema.str_schema())}, + ), + ) + ) + } + ), + ) + + inner_validator = SchemaValidator(inner_schema) + outer_validator = SchemaValidator(outer_schema) + + # the custom validation function does apply for the inner model + result_inner = inner_validator.validate_python({'x': 'hello'}) + assert result_inner.x == 'hello modified' + + # but the outer model doesn't reuse the custom wrap validator function, so we see simple str val + result_outer = outer_validator.validate_python({'inner': {'x': 'hello'}}) + assert result_outer.inner.x == 'hello' + + +def test_reuse_plain_serializer_ok() -> None: + class InnerModel: + x: str + + def __init__(self, x: str) -> None: + self.x = x + + def serialize_inner(v: InnerModel) -> str: + return v.x + ' modified' + + inner_schema = core_schema.model_schema( + InnerModel, + schema=core_schema.model_fields_schema( + {'x': core_schema.model_field(schema=core_schema.str_schema())}, + ), + serialization=core_schema.plain_serializer_function_ser_schema(serialize_inner), + ) + + inner_schema_serializer = SchemaSerializer(inner_schema) + InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue] + InnerModel.__pydantic_serializer__ = inner_schema_serializer # pyright: ignore[reportAttributeAccessIssue] + + class OuterModel: + inner: InnerModel + + def __init__(self, inner: InnerModel) -> None: + self.inner = inner + + outer_schema = core_schema.model_schema( + OuterModel, + schema=core_schema.model_fields_schema( + { + 'inner': core_schema.model_field( + schema=core_schema.model_schema( + InnerModel, + schema=core_schema.model_fields_schema( + # note, we use a simple str schema (with no custom serialization) + # in order to verify that the prebuilt serializer from InnerModel is used instead + {'x': core_schema.model_field(schema=core_schema.str_schema())}, + ), + ) + ) + } + ), + ) + + inner_serializer = SchemaSerializer(inner_schema) + outer_serializer = SchemaSerializer(outer_schema) + + # the custom serialization function does apply for the inner model + inner_instance = InnerModel(x='hello') + assert inner_serializer.to_python(inner_instance) == 'hello modified' + assert 'FunctionPlainSerializer' in repr(inner_serializer) + + # the custom ser function applies for the outer model as well, a plain serializer is permitted as a prebuilt candidate + outer_instance = OuterModel(inner=InnerModel(x='hello')) + assert outer_serializer.to_python(outer_instance) == {'inner': 'hello modified'} + assert 'PrebuiltSerializer' in repr(outer_serializer) + + +def test_reuse_plain_validator_ok() -> None: + class InnerModel: + x: str + + def __init__(self, x: str) -> None: + self.x = x + + def validate_inner(data) -> InnerModel: + data['x'] = data['x'] + ' modified' + return InnerModel(**data) + + inner_schema = core_schema.no_info_plain_validator_function(validate_inner) + + inner_schema_validator = SchemaValidator(inner_schema) + InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue] + InnerModel.__pydantic_validator__ = inner_schema_validator # pyright: ignore[reportAttributeAccessIssue] + + class OuterModel: + inner: InnerModel + + def __init__(self, inner: InnerModel) -> None: + self.inner = inner + + outer_schema = core_schema.model_schema( + OuterModel, + schema=core_schema.model_fields_schema( + { + 'inner': core_schema.model_field( + schema=core_schema.model_schema( + InnerModel, + schema=core_schema.model_fields_schema( + # note, we use a simple str schema (with no custom validation) + # in order to verify that the prebuilt validator from InnerModel is used instead + {'x': core_schema.model_field(schema=core_schema.str_schema())}, + ), + ) + ) + } + ), + ) + + inner_validator = SchemaValidator(inner_schema) + outer_validator = SchemaValidator(outer_schema) + + # the custom validation function does apply for the inner model + result_inner = inner_validator.validate_python({'x': 'hello'}) + assert result_inner.x == 'hello modified' + assert 'FunctionPlainValidator' in repr(inner_validator) + + # the custom validation function does apply for the outer model as well, a plain validator is permitted as a prebuilt candidate + result_outer = outer_validator.validate_python({'inner': {'x': 'hello'}}) + assert result_outer.inner.x == 'hello modified' + assert 'PrebuiltValidator' in repr(outer_validator)