Skip to content

Commit 3bc191a

Browse files
dhruvildarjiclaudesloria
authored
Fix Field.error_messages type to allow dict and list values (#2907)
* Fix Field.error_messages type to allow dict and list values The error_messages parameter on Field and its type annotation were typed as dict[str, str], but error messages can actually be dicts or lists (as used in tests and supported by ValidationError). This updates the type to dict[str, str | list | dict] via a new ErrorMessages type alias, fixing #1636. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address type errors; add type annotation to catch regression * Update changelog and authors --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Steven Loria <git@stevenloria.com>
1 parent c530f85 commit 3bc191a

5 files changed

Lines changed: 21 additions & 5 deletions

File tree

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,4 @@ Contributors (chronological)
187187
- Fridayworks `@worksbyfriday <https://github.com/worksbyfriday>`_
188188
- `@rstar327 <https://github.com/rstar327>`_
189189
- Kadir Can Ozden `@bysiber <https://github.com/bysiber>`_
190+
- Dhruvil Darji `@dhruvildarji <https://github.com/dhruvildarji>`_

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Bug fixes:
1818
Thanks :user:`bysiber` for the PR.
1919
- `marshmallow.fields.DateTime` with ``format="timestamp_ms"`` properly
2020
rejects bool values (:pr:`2904`). Thanks :user:`bysiber` for the PR.
21+
- Fix typing of ``error_essages`` argument to `marshmallow.fields.Field` (:pr:`1636`).
22+
Thanks :user:`repole` for reporting and :user:`dhruvildarji` for the PR.
2123

2224
4.2.2 (2026-02-04)
2325
------------------

src/marshmallow/fields.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ class _BaseFieldKwargs(typing.TypedDict, total=False):
9494
allow_none: bool | None
9595
load_only: bool
9696
dump_only: bool
97-
error_messages: dict[str, str] | None
97+
error_messages: types.ErrorMessages | None
9898
metadata: typing.Mapping[str, typing.Any] | None
9999

100100

@@ -169,7 +169,7 @@ class Field(typing.Generic[_InternalT]):
169169
#: Default error messages for various kinds of errors. The keys in this dictionary
170170
#: are passed to `Field.make_error`. The values are error messages passed to
171171
#: :exc:`marshmallow.exceptions.ValidationError`.
172-
default_error_messages: dict[str, str] = {
172+
default_error_messages: types.ErrorMessages = {
173173
"required": "Missing data for required field.",
174174
"null": "Field may not be null.",
175175
"validator_failed": "Invalid value.",
@@ -187,7 +187,7 @@ def __init__(
187187
allow_none: bool | None = None,
188188
load_only: bool = False,
189189
dump_only: bool = False,
190-
error_messages: dict[str, str] | None = None,
190+
error_messages: types.ErrorMessages | None = None,
191191
metadata: typing.Mapping[str, typing.Any] | None = None,
192192
) -> None:
193193
self.dump_default = dump_default
@@ -220,7 +220,7 @@ def __init__(
220220
metadata = metadata or {}
221221
self.metadata = metadata
222222
# Collect default error message from self and parent classes
223-
messages: dict[str, str] = {}
223+
messages: types.ErrorMessages = {}
224224
for cls in reversed(self.__class__.__mro__):
225225
messages.update(getattr(cls, "default_error_messages", {}))
226226
messages.update(error_messages or {})
@@ -1732,6 +1732,8 @@ def __init__(
17321732
self.absolute = absolute
17331733
self.require_tld = require_tld
17341734
# Insert validation into self.validators so that multiple errors can be stored.
1735+
if not isinstance(self.error_messages["invalid"], str):
1736+
raise ValueError('"invalid" error message must be a string.')
17351737
validator = validate.URL(
17361738
relative=self.relative,
17371739
absolute=self.absolute,
@@ -1755,6 +1757,8 @@ class Email(String):
17551757
def __init__(self, **kwargs: Unpack[_BaseFieldKwargs]) -> None:
17561758
super().__init__(**kwargs)
17571759
# Insert validation into self.validators so that multiple errors can be stored.
1760+
if not isinstance(self.error_messages["invalid"], str):
1761+
raise ValueError('"invalid" error message must be a string.')
17581762
validator = validate.Email(error=self.error_messages["invalid"])
17591763
self.validators.insert(0, validator)
17601764

src/marshmallow/types.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
#: Type for validator functions
1616
Validator: typing.TypeAlias = typing.Callable[[typing.Any], typing.Any]
1717

18+
#: Type for a single error message value, which can be a string, list, or dict
19+
ErrorMessageValue: typing.TypeAlias = str | list | dict
20+
21+
#: Type for error_messages dictionaries passed to fields
22+
ErrorMessages: typing.TypeAlias = dict[str, ErrorMessageValue]
23+
1824
#: A valid option for the ``unknown`` schema option and argument
1925
UnknownOption: typing.TypeAlias = typing.Literal["exclude", "include", "raise"]
2026

tests/test_deserialization.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import decimal
44
import ipaddress
55
import math
6+
import typing
67
import uuid
78
from unittest.mock import patch
89

@@ -2363,7 +2364,9 @@ class RequireSchema(Schema):
23632364
["first error", "second error"],
23642365
],
23652366
)
2366-
def test_required_message_can_be_changed(message):
2367+
def test_required_message_can_be_changed(
2368+
message: str | dict[str, typing.Any] | list[str],
2369+
):
23672370
class RequireSchema(Schema):
23682371
age = fields.Integer(required=True, error_messages={"required": message})
23692372

0 commit comments

Comments
 (0)