diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8cd5d977e..646defb85 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -130,6 +130,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/pyproject.toml b/pyproject.toml index f4e582473..40bc2ea0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ include = [ exclude = ["docs/_build/"] [tool.ruff] -src = ["src"] +src = ["src", "tests", "examples"] fix = true show-fixes = true output-format = "full" diff --git a/src/marshmallow/schema.py b/src/marshmallow/schema.py index b42b3a09d..eb0cba04c 100644 --- a/src/marshmallow/schema.py +++ b/src/marshmallow/schema.py @@ -28,7 +28,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 get_value, is_collection, set_value @@ -765,16 +765,16 @@ def loads( def _run_validator( self, - validator_func, + validator_func: types.SchemaValidator, output, *, original_data, - error_store, - many, - partial, - unknown, - pass_original, - index=None, + error_store: ErrorStore, + many: bool, + partial: bool | types.StrSequenceOrSet | None, + unknown: types.UnknownOption | None, + pass_original: bool, + index: int | None = None, ): try: if pass_original: # Pass original, raw data (before unmarshalling) @@ -784,7 +784,26 @@ def _run_validator( else: validator_func(output, partial=partial, many=many, unknown=unknown) 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 7a60bef1d..8f060baaf 100644 --- a/src/marshmallow/types.py +++ b/src/marshmallow/types.py @@ -26,6 +26,18 @@ UnknownOption: TypeAlias = typing.Literal["exclude", "include", "raise"] +class SchemaValidator(typing.Protocol): + def __call__( + self, + output: typing.Any, + original_data: typing.Any = ..., + *, + partial: bool | StrSequenceOrSet | None = None, + unknown: UnknownOption | 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 3f3dd69ee..a35ceb128 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -676,6 +676,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):