Skip to content

Commit

Permalink
Make Field a generic type; refactor inheritance hierarchies (#2725)
Browse files Browse the repository at this point in the history
* Make Field a generic type; refactor inheritance hierarchies

* Remove invalid tests and fix temporal fields

* Attempt to fix py39

* More py39 fixes

* Remove unnecessary _serialize override in Boolean

* Remove unnecessary _serialize override in String

* Revert "Remove unnecessary _serialize override in String"

This reverts commit b4f4718.

* Update Field docstring to be accurate

* Update usages of fields.Field in exampples and tests

* Add overrides to specify when None vs internal type is returned

* Explicitly document fields that shouldn't be used within schemas

* Update upgrading guide

* Fix typo

* Remove 'Base', for consistency with other base classes
  • Loading branch information
sloria authored Jan 5, 2025
1 parent d2d26a1 commit 68c68e3
Show file tree
Hide file tree
Showing 13 changed files with 397 additions and 310 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ Features:

Other changes:

- Typing: `Field <marshmallow.fields.Field>` is now a generic type with a type argument for the internal value type.
Therefore, it is no longer usable as a field in a schema. Use a subclass of `Field <marshmallow.fields.Field>` instead.
- `marshmallow.fields.UUID` no longer subclasses `marshmallow.fields.String`.
- *Backwards-incompatible*: `marshmallow.fields.Number` is no longer usable as a field in a schema.
Use `marshmallow.fields.Integer`, `marshmallow.fields.Float`, or `marshmallow.fields.Decimal` instead.
- *Backwards-incompatible*: `marshmallow.fields.Mapping` is no longer usable as a field in a schema.
Use `marshmallow.fields.Dict` instead.
- *Backwards-incompatible*: Use `datetime.date.fromisoformat`, `datetime.time.fromisoformat`, and `datetime.datetime.fromisoformat` from the standard library to deserialize dates, times and datetimes (:pr:`2078`).
- *Backwards-incompatible*: `marshmallow.fields.Boolean` no longer serializes non-boolean values.

As a consequence of this change:
- Time with time offsets are now supported.
Expand Down
3 changes: 2 additions & 1 deletion docs/custom_fields.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ Creating a field class
----------------------

To create a custom field class, create a subclass of :class:`marshmallow.fields.Field` and implement its :meth:`_serialize <marshmallow.fields.Field._serialize>` and/or :meth:`_deserialize <marshmallow.fields.Field._deserialize>` methods.
Field's type argument is the internal type, i.e. the type that the field deserializes to.

.. code-block:: python
from marshmallow import fields, ValidationError
class PinCode(fields.Field):
class PinCode(fields.Field[list[int]]):
"""Field that serializes to a string of numbers and deserializes
to a list of numbers.
"""
Expand Down
4 changes: 2 additions & 2 deletions docs/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ The pipeline for serialization is similar, except that the ``pass_collection=Tru
# YES
class MySchema(Schema):
field_a = fields.Field()
field_a = fields.Raw()
@pre_load
def preprocess(self, data, **kwargs):
Expand All @@ -202,7 +202,7 @@ The pipeline for serialization is similar, except that the ``pass_collection=Tru
# NO
class MySchema(Schema):
field_a = fields.Field()
field_a = fields.Raw()
@pre_load
def step1(self, data, **kwargs):
Expand Down
103 changes: 85 additions & 18 deletions docs/upgrading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,73 @@ This section documents migration paths to new releases.
Upgrading to 4.0
++++++++++++++++

``Field`` usage
***************

`Field <marshmallow.fields.Field>` is the base class for all fields and should not be used directly within schemas.
Only use subclasses of `Field <marshmallow.fields.Field>` in your schemas.

.. code-block:: python
from marshmallow import Schema, fields
# 3.x
class UserSchema(Schema):
name = fields.Field()
# 4.x
class UserSchema(Schema):
name = fields.String()
`Field <marshmallow.fields.Field>` is a generic class with a type argument.
When defining a custom field, the type argument should be used to specify the internal type.

.. code-block:: python
from marshmallow import fields
class PinCode(fields.Field[list[int]]):
"""Field that serializes to a string of numbers and deserializes
to a list of numbers.
"""
def _serialize(self, value, attr, obj, **kwargs):
if value is None:
return ""
return "".join(str(d) for d in value)
# The return type is inferred to be list[int]
def _deserialize(self, value, attr, data, **kwargs):
try:
return [int(c) for c in value]
except ValueError as error:
raise ValidationError("Pin codes must contain only digits.") from error
``Number`` and ``Mapping`` fields as base classes
*************************************************

`Number <marshmallow.fields.Number>` and `Mapping <marshmallow.fields.Mapping>` are bases classes that should not be used within schemas.
Use their subclasses instead.

.. code-block:: python
from marshmallow import Schema, fields
# 3.x
class PackageSchema(Schema):
revision = fields.Number()
dependencies = fields.Mapping()
# 4.x
class PackageSchema(Schema):
revision = fields.Integer()
dependencies = fields.Dict()
Validators must raise a ValidationError
***************************************

Expand Down Expand Up @@ -215,8 +282,8 @@ Custom fields that define a `_bind_to_schema <marshmallow.Fields._bind_to_schema
class MyField(fields.Field):
def _bind_to_schema(self, parent, field_name): ...
Use standard library for parsing ISO 8601 dates, times, and datetimes
*********************************************************************
Use standard library functions for parsing ISO 8601 dates, times, and datetimes
*******************************************************************************

The ``from_iso_*`` utilities are removed from marshmallow in favor of using the standard library implementations.

Expand Down Expand Up @@ -1230,8 +1297,8 @@ The ``prefix`` parameter of ``Schema`` is removed. The same feature can be achie
# 2.x
class MySchema(Schema):
f1 = fields.Field()
f2 = fields.Field()
f1 = fields.Raw()
f2 = fields.Raw()
MySchema(prefix="pre_").dump({"f1": "one", "f2": "two"})
Expand All @@ -1240,8 +1307,8 @@ The ``prefix`` parameter of ``Schema`` is removed. The same feature can be achie
# 3.x
class MySchema(Schema):
f1 = fields.Field()
f2 = fields.Field()
f1 = fields.Raw()
f2 = fields.Raw()
@post_dump
def prefix_usr(self, data):
Expand Down Expand Up @@ -1285,10 +1352,10 @@ In marshmallow 2, it was possible to have multiple fields with the same ``attrib
# 2.x
class MySchema(Schema):
f1 = fields.Field()
f2 = fields.Field(attribute="f1")
f3 = fields.Field(attribute="f5")
f4 = fields.Field(attribute="f5")
f1 = fields.Raw()
f2 = fields.Raw(attribute="f1")
f3 = fields.Raw(attribute="f5")
f4 = fields.Raw(attribute="f5")
MySchema()
Expand All @@ -1297,21 +1364,21 @@ In marshmallow 2, it was possible to have multiple fields with the same ``attrib
# 3.x
class MySchema(Schema):
f1 = fields.Field()
f2 = fields.Field(attribute="f1")
f3 = fields.Field(attribute="f5")
f4 = fields.Field(attribute="f5")
f1 = fields.Raw()
f2 = fields.Raw(attribute="f1")
f3 = fields.Raw(attribute="f5")
f4 = fields.Raw(attribute="f5")
MySchema()
# ValueError: 'Duplicate attributes: ['f1', 'f5]'
class MySchema(Schema):
f1 = fields.Field()
f2 = fields.Field(attribute="f1", dump_only=True)
f3 = fields.Field(attribute="f5")
f4 = fields.Field(attribute="f5", dump_only=True)
f1 = fields.Raw()
f2 = fields.Raw(attribute="f1", dump_only=True)
f3 = fields.Raw(attribute="f5")
f4 = fields.Raw(attribute="f5", dump_only=True)
MySchema()
Expand Down
2 changes: 1 addition & 1 deletion examples/package_json_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from marshmallow import INCLUDE, Schema, ValidationError, fields


class Version(fields.Field):
class Version(fields.Field[version.Version]):
"""Version field that deserializes to a Version object."""

def _deserialize(self, value, *args, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion src/marshmallow/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(
data: typing.Mapping[str, typing.Any]
| typing.Iterable[typing.Mapping[str, typing.Any]]
| None = None,
valid_data: list[dict[str, typing.Any]] | dict[str, typing.Any] | None = None,
valid_data: list[typing.Any] | dict[str, typing.Any] | None = None,
**kwargs,
):
self.messages = [message] if isinstance(message, (str, bytes)) else message
Expand Down
Loading

0 comments on commit 68c68e3

Please sign in to comment.