Skip to content
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
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,4 @@ Contributors (chronological)
- Fridayworks `@worksbyfriday <https://github.com/worksbyfriday>`_
- `@rstar327 <https://github.com/rstar327>`_
- Kadir Can Ozden `@bysiber <https://github.com/bysiber>`_
- Dhruvil Darji `@dhruvildarji <https://github.com/dhruvildarji>`_
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Bug fixes:
Thanks :user:`bysiber` for the PR.
- `marshmallow.fields.DateTime` with ``format="timestamp_ms"`` properly
rejects bool values (:pr:`2904`). Thanks :user:`bysiber` for the PR.
- Fix typing of ``error_essages`` argument to `marshmallow.fields.Field` (:pr:`1636`).
Thanks :user:`repole` for reporting and :user:`dhruvildarji` for the PR.

4.2.2 (2026-02-04)
------------------
Expand Down
12 changes: 8 additions & 4 deletions src/marshmallow/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class _BaseFieldKwargs(typing.TypedDict, total=False):
allow_none: bool | None
load_only: bool
dump_only: bool
error_messages: dict[str, str] | None
error_messages: types.ErrorMessages | None
metadata: typing.Mapping[str, typing.Any] | None


Expand Down Expand Up @@ -169,7 +169,7 @@ class Field(typing.Generic[_InternalT]):
#: Default error messages for various kinds of errors. The keys in this dictionary
#: are passed to `Field.make_error`. The values are error messages passed to
#: :exc:`marshmallow.exceptions.ValidationError`.
default_error_messages: dict[str, str] = {
default_error_messages: types.ErrorMessages = {
"required": "Missing data for required field.",
"null": "Field may not be null.",
"validator_failed": "Invalid value.",
Expand All @@ -187,7 +187,7 @@ def __init__(
allow_none: bool | None = None,
load_only: bool = False,
dump_only: bool = False,
error_messages: dict[str, str] | None = None,
error_messages: types.ErrorMessages | None = None,
metadata: typing.Mapping[str, typing.Any] | None = None,
) -> None:
self.dump_default = dump_default
Expand Down Expand Up @@ -220,7 +220,7 @@ def __init__(
metadata = metadata or {}
self.metadata = metadata
# Collect default error message from self and parent classes
messages: dict[str, str] = {}
messages: types.ErrorMessages = {}
for cls in reversed(self.__class__.__mro__):
messages.update(getattr(cls, "default_error_messages", {}))
messages.update(error_messages or {})
Expand Down Expand Up @@ -1732,6 +1732,8 @@ def __init__(
self.absolute = absolute
self.require_tld = require_tld
# Insert validation into self.validators so that multiple errors can be stored.
if not isinstance(self.error_messages["invalid"], str):
raise ValueError('"invalid" error message must be a string.')
validator = validate.URL(
relative=self.relative,
absolute=self.absolute,
Expand All @@ -1755,6 +1757,8 @@ class Email(String):
def __init__(self, **kwargs: Unpack[_BaseFieldKwargs]) -> None:
super().__init__(**kwargs)
# Insert validation into self.validators so that multiple errors can be stored.
if not isinstance(self.error_messages["invalid"], str):
raise ValueError('"invalid" error message must be a string.')
validator = validate.Email(error=self.error_messages["invalid"])
self.validators.insert(0, validator)

Expand Down
6 changes: 6 additions & 0 deletions src/marshmallow/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
#: Type for validator functions
Validator: typing.TypeAlias = typing.Callable[[typing.Any], typing.Any]

#: Type for a single error message value, which can be a string, list, or dict
ErrorMessageValue: typing.TypeAlias = str | list | dict

#: Type for error_messages dictionaries passed to fields
ErrorMessages: typing.TypeAlias = dict[str, ErrorMessageValue]

#: A valid option for the ``unknown`` schema option and argument
UnknownOption: typing.TypeAlias = typing.Literal["exclude", "include", "raise"]

Expand Down
5 changes: 4 additions & 1 deletion tests/test_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import decimal
import ipaddress
import math
import typing
import uuid
from unittest.mock import patch

Expand Down Expand Up @@ -2363,7 +2364,9 @@ class RequireSchema(Schema):
["first error", "second error"],
],
)
def test_required_message_can_be_changed(message):
def test_required_message_can_be_changed(
message: str | dict[str, typing.Any] | list[str],
):
class RequireSchema(Schema):
age = fields.Integer(required=True, error_messages={"required": message})

Expand Down