Repository: marshmallow-code/marshmallow
Pull request #2792 (2025-01-19 17:55:11)

Fix handling data_key in ValidationErrors raised in schema validators
closes https://github.com/marshmallow-code/marshmallow/issues/2170


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 144ff9af..5a9c176f 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 <marshmallow.exceptions.ValidationError>` 
+  with a ``field_name`` argument (:issue:`2170`). Thanks :user:`matejsp` for reporting.
+
 Documentation:
 
 - Add :doc:`upgrading guides <upgrading>` for 3.24 and 3.26 (:pr:`2780`).
diff --git a/src/marshmallow/decorators.py b/src/marshmallow/decorators.py
index eefba6c1..5ffa3f4f 100644
--- a/src/marshmallow/decorators.py
+++ b/src/marshmallow/decorators.py
@@ -69,7 +69,10 @@ from __future__ import annotations
 
 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 6009a436..44c15d5b 100644
--- a/src/marshmallow/schema.py
+++ b/src/marshmallow/schema.py
@@ -27,7 +27,7 @@ from marshmallow.decorators import (
     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 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
 
     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 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
             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 97cb44c1..599f6b49 100644
--- a/src/marshmallow/types.py
+++ b/src/marshmallow/types.py
@@ -16,6 +16,17 @@ StrSequenceOrSet = typing.Union[typing.Sequence[str], typing.AbstractSet[str]]
 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


Test patch:
diff --git a/tests/test_decorators.py b/tests/test_decorators.py
index a97bf591..0a305405 100644
--- a/tests/test_decorators.py
+++ b/tests/test_decorators.py
@@ -689,6 +689,34 @@ class TestValidatesSchemaDecorator:
         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):