From 9f751e1cef943e2ca1a77f93d3483b4a171d4303 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Sun, 19 Jan 2025 12:58:10 -0500 Subject: [PATCH] Fix handling data_key in ValidationErrors raised in schema validators (#2792) --- CHANGELOG.rst | 5 +++++ src/marshmallow/decorators.py | 7 +++++-- src/marshmallow/schema.py | 35 +++++++++++++++++++++++++++-------- src/marshmallow/types.py | 11 +++++++++++ tests/test_decorators.py | 28 ++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 144ff9af4..5a9c176f0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,11 @@ Features: - Typing: Improve type coverage of `marshmallow.Schema.SchemaMeta` (:pr:`2761`). - Typing: `marshmallow.Schema.loads` parameter allows `bytes` and `bytesarray` (:pr:`2769`). +Bug fixes: + +- Respect ``data_key`` when schema validators raise a `ValidationError ` + with a ``field_name`` argument (:issue:`2170`). Thanks :user:`matejsp` for reporting. + Documentation: - Add :doc:`upgrading guides ` for 3.24 and 3.26 (:pr:`2780`). diff --git a/src/marshmallow/decorators.py b/src/marshmallow/decorators.py index eefba6c19..5ffa3f4fd 100644 --- a/src/marshmallow/decorators.py +++ b/src/marshmallow/decorators.py @@ -69,7 +69,10 @@ def validate_age(self, data, **kwargs): import functools from collections import defaultdict -from typing import Any, Callable, cast +from typing import TYPE_CHECKING, Any, Callable, cast + +if TYPE_CHECKING: + from marshmallow import types PRE_DUMP = "pre_dump" POST_DUMP = "post_dump" @@ -92,7 +95,7 @@ def validates(field_name: str) -> Callable[..., Any]: def validates_schema( - fn: Callable[..., Any] | None = None, + fn: types.SchemaValidator | None = None, pass_many: bool = False, # noqa: FBT001, FBT002 pass_original: bool = False, # noqa: FBT001, FBT002 skip_on_field_errors: bool = True, # noqa: FBT001, FBT002 diff --git a/src/marshmallow/schema.py b/src/marshmallow/schema.py index 6009a436a..44c15d5b6 100644 --- a/src/marshmallow/schema.py +++ b/src/marshmallow/schema.py @@ -27,7 +27,7 @@ VALIDATES_SCHEMA, ) from marshmallow.error_store import ErrorStore -from marshmallow.exceptions import StringNotCollectionError, ValidationError +from marshmallow.exceptions import SCHEMA, StringNotCollectionError, ValidationError from marshmallow.orderedset import OrderedSet from marshmallow.utils import ( EXCLUDE, @@ -824,15 +824,15 @@ def loads( def _run_validator( self, - validator_func, + validator_func: types.SchemaValidator, output, *, original_data, - error_store, - many, - partial, - pass_original, - index=None, + error_store: ErrorStore, + many: bool, + partial: bool | types.StrSequenceOrSet | None, + pass_original: bool, + index: int | None = None, ): try: if pass_original: # Pass original, raw data (before unmarshalling) @@ -840,7 +840,26 @@ def _run_validator( else: validator_func(output, partial=partial, many=many) except ValidationError as err: - error_store.store_error(err.messages, err.field_name, index=index) + field_name = err.field_name + data_key: str + if field_name == SCHEMA: + data_key = SCHEMA + else: + field_obj: Field | None = None + try: + field_obj = self.fields[field_name] + except KeyError: + if field_name in self.declared_fields: + field_obj = self.declared_fields[field_name] + if field_obj: + data_key = ( + field_obj.data_key + if field_obj.data_key is not None + else field_name + ) + else: + data_key = field_name + error_store.store_error(err.messages, data_key, index=index) def validate( self, diff --git a/src/marshmallow/types.py b/src/marshmallow/types.py index 97cb44c12..599f6b49e 100644 --- a/src/marshmallow/types.py +++ b/src/marshmallow/types.py @@ -16,6 +16,17 @@ Validator = typing.Callable[[typing.Any], typing.Any] +class SchemaValidator(typing.Protocol): + def __call__( + self, + output: typing.Any, + original_data: typing.Any = ..., + *, + partial: bool | StrSequenceOrSet | None = None, + many: bool = False, + ) -> None: ... + + class RenderModule(typing.Protocol): def dumps( self, obj: typing.Any, *args: typing.Any, **kwargs: typing.Any diff --git a/tests/test_decorators.py b/tests/test_decorators.py index a97bf5916..0a3054051 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -689,6 +689,34 @@ def validate_many(self, data, many, **kwargs): assert "bar" in errors[0] assert "_schema" not in errors + # https://github.com/marshmallow-code/marshmallow/issues/2170 + def test_data_key_is_used_in_errors_dict(self): + class MySchema(Schema): + foo = fields.Int(data_key="fooKey") + + @validates("foo") + def validate_foo(self, value, **kwargs): + raise ValidationError("from validates") + + @validates_schema(skip_on_field_errors=False) + def validate_schema(self, data, **kwargs): + raise ValidationError("from validates_schema str", field_name="foo") + + @validates_schema(skip_on_field_errors=False) + def validate_schema2(self, data, **kwargs): + raise ValidationError({"fooKey": "from validates_schema dict"}) + + with pytest.raises(ValidationError) as excinfo: + MySchema().load({"fooKey": 42}) + exc = excinfo.value + assert exc.messages == { + "fooKey": [ + "from validates", + "from validates_schema str", + "from validates_schema dict", + ] + } + def test_decorator_error_handling(): class ExampleSchema(Schema):