Skip to content

Skip reusing after validators in prebuilt schemas #1663

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 6, 2025
Merged
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
5 changes: 4 additions & 1 deletion src/validators/prebuilt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ impl PrebuiltValidator {
pub fn try_get_from_schema(type_: &str, schema: &Bound<'_, PyDict>) -> PyResult<Option<CombinedValidator>> {
get_prebuilt(type_, schema, "__pydantic_validator__", |py_any| {
let schema_validator = py_any.extract::<Py<SchemaValidator>>()?;
if matches!(schema_validator.get().validator, CombinedValidator::FunctionWrap(_)) {
if matches!(
schema_validator.get().validator,
CombinedValidator::FunctionWrap(_) | CombinedValidator::FunctionAfter(_)
) {
return Ok(None);
}
Ok(Some(Self { schema_validator }.into()))
Expand Down
152 changes: 136 additions & 16 deletions tests/test_prebuilt.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ class InnerModel:
),
)

inner_schema_validator = SchemaValidator(inner_schema)
inner_schema_serializer = SchemaSerializer(inner_schema)
inner_validator = SchemaValidator(inner_schema)
inner_serializer = SchemaSerializer(inner_schema)
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
InnerModel.__pydantic_validator__ = inner_schema_validator # pyright: ignore[reportAttributeAccessIssue]
InnerModel.__pydantic_serializer__ = inner_schema_serializer # pyright: ignore[reportAttributeAccessIssue]
InnerModel.__pydantic_validator__ = inner_validator # pyright: ignore[reportAttributeAccessIssue]
InnerModel.__pydantic_serializer__ = inner_serializer # pyright: ignore[reportAttributeAccessIssue]

class OuterModel:
inner: InnerModel
Expand Down Expand Up @@ -69,9 +69,9 @@ def serialize_inner(v: InnerModel, serializer) -> Union[dict[str, str], str]:
serialization=core_schema.wrap_serializer_function_ser_schema(serialize_inner),
)

inner_schema_serializer = SchemaSerializer(inner_schema)
inner_serializer = SchemaSerializer(inner_schema)
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
InnerModel.__pydantic_serializer__ = inner_schema_serializer # pyright: ignore[reportAttributeAccessIssue]
InnerModel.__pydantic_serializer__ = inner_serializer # pyright: ignore[reportAttributeAccessIssue]

class OuterModel:
inner: InnerModel
Expand All @@ -97,7 +97,6 @@ def __init__(self, inner: InnerModel) -> None:
),
)

inner_serializer = SchemaSerializer(inner_schema)
outer_serializer = SchemaSerializer(outer_schema)

# the custom serialization function does apply for the inner model
Expand Down Expand Up @@ -130,9 +129,9 @@ def validate_inner(data, validator) -> InnerModel:
),
)

inner_schema_validator = SchemaValidator(inner_schema)
inner_validator = SchemaValidator(inner_schema)
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
InnerModel.__pydantic_validator__ = inner_schema_validator # pyright: ignore[reportAttributeAccessIssue]
InnerModel.__pydantic_validator__ = inner_validator # pyright: ignore[reportAttributeAccessIssue]

class OuterModel:
inner: InnerModel
Expand All @@ -158,7 +157,6 @@ def __init__(self, inner: InnerModel) -> None:
),
)

inner_validator = SchemaValidator(inner_schema)
outer_validator = SchemaValidator(outer_schema)

# the custom validation function does apply for the inner model
Expand All @@ -170,6 +168,66 @@ def __init__(self, inner: InnerModel) -> None:
assert result_outer.inner.x == 'hello'


def test_prebuilt_not_used_for_after_validator_functions() -> None:
class InnerModel:
x: str

def __init__(self, x: str) -> None:
self.x = x

def validate_after(self) -> InnerModel:
self.x = self.x + ' modified'
return self

inner_schema = core_schema.no_info_after_validator_function(
validate_after,
core_schema.model_schema(
InnerModel,
schema=core_schema.model_fields_schema(
{'x': core_schema.model_field(schema=core_schema.str_schema())},
),
),
)

inner_validator = SchemaValidator(inner_schema)
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
InnerModel.__pydantic_validator__ = inner_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())},
),
)
)
}
),
)

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 after 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
Expand All @@ -188,9 +246,9 @@ def serialize_inner(v: InnerModel) -> str:
serialization=core_schema.plain_serializer_function_ser_schema(serialize_inner),
)

inner_schema_serializer = SchemaSerializer(inner_schema)
inner_serializer = SchemaSerializer(inner_schema)
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
InnerModel.__pydantic_serializer__ = inner_schema_serializer # pyright: ignore[reportAttributeAccessIssue]
InnerModel.__pydantic_serializer__ = inner_serializer # pyright: ignore[reportAttributeAccessIssue]

class OuterModel:
inner: InnerModel
Expand All @@ -216,7 +274,6 @@ def __init__(self, inner: InnerModel) -> None:
),
)

inner_serializer = SchemaSerializer(inner_schema)
outer_serializer = SchemaSerializer(outer_schema)

# the custom serialization function does apply for the inner model
Expand All @@ -243,9 +300,9 @@ def validate_inner(data) -> InnerModel:

inner_schema = core_schema.no_info_plain_validator_function(validate_inner)

inner_schema_validator = SchemaValidator(inner_schema)
inner_validator = SchemaValidator(inner_schema)
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
InnerModel.__pydantic_validator__ = inner_schema_validator # pyright: ignore[reportAttributeAccessIssue]
InnerModel.__pydantic_validator__ = inner_validator # pyright: ignore[reportAttributeAccessIssue]

class OuterModel:
inner: InnerModel
Expand All @@ -271,7 +328,6 @@ def __init__(self, inner: InnerModel) -> None:
),
)

inner_validator = SchemaValidator(inner_schema)
outer_validator = SchemaValidator(outer_schema)

# the custom validation function does apply for the inner model
Expand All @@ -283,3 +339,67 @@ def __init__(self, inner: InnerModel) -> None:
result_outer = outer_validator.validate_python({'inner': {'x': 'hello'}})
assert result_outer.inner.x == 'hello modified'
assert 'PrebuiltValidator' in repr(outer_validator)


def test_reuse_before_validator_ok() -> None:
class InnerModel:
x: str

def __init__(self, x: str) -> None:
self.x = x

def validate_before(data) -> dict:
data['x'] = data['x'] + ' modified'
return data

inner_schema = core_schema.no_info_before_validator_function(
validate_before,
core_schema.model_schema(
InnerModel,
schema=core_schema.model_fields_schema(
{'x': core_schema.model_field(schema=core_schema.str_schema())},
),
),
)

inner_validator = SchemaValidator(inner_schema)
InnerModel.__pydantic_complete__ = True # pyright: ignore[reportAttributeAccessIssue]
InnerModel.__pydantic_validator__ = inner_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())},
),
)
)
}
),
)

outer_validator = SchemaValidator(outer_schema)
print(inner_validator)
print(outer_validator)

# 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 'FunctionBeforeValidator' in repr(inner_validator)

# the custom validation function does apply for the outer model as well, a before 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)
Loading