Skip to content

Commit bd60b4a

Browse files
committed
Update TimeDelta field type to float for Marshmallow v4
In Marshmallow v3.17.0, up until v4, the `TimeDelta` field could be serialized into either an integer or a float, depending on user specification of the field. [1] In the case the user set `serialization_type=float`, apispec would generate the wrong JSON type in the API docs. With Marshmallow v4, this was removed and `TimeDelta` fields *always* serialize to floats, in which case apispec always generates the wrong docs. This commit updates the default field mapping anticipating Marshmallow v4's float serialization, with some additional support added for <v4 by looking for the deprecated and removed `serialization_type` attribute on the field object. [1] https://marshmallow.readthedocs.io/en/stable/marshmallow.fields.html#marshmallow.fields.TimeDelta
1 parent f51f13f commit bd60b4a

File tree

3 files changed

+34
-2
lines changed

3 files changed

+34
-2
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,4 @@ Contributors (chronological)
8787
- Robert Shepley `@ShepleySound <https://github.com/ShepleySound>`_
8888
- Robin `@allrob23 <https://github.com/allrob23>`_
8989
- Xingang Zhang `@0x0400 <https://github.com/0x0400>`_
90+
- Lewis Haley `@LewisHaley <https://github.com/LewisHaley>`_

src/apispec/ext/marshmallow/field_converter.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
marshmallow.fields.DateTime: ("string", "date-time"),
3232
marshmallow.fields.Date: ("string", "date"),
3333
marshmallow.fields.Time: ("string", None),
34-
marshmallow.fields.TimeDelta: ("integer", None),
34+
marshmallow.fields.TimeDelta: ("number", None),
3535
marshmallow.fields.Email: ("string", "email"),
3636
marshmallow.fields.URL: ("string", "url"),
3737
marshmallow.fields.Dict: ("object", None),
@@ -538,6 +538,13 @@ def timedelta2properties(self, field, **kwargs: typing.Any) -> dict:
538538
ret = {}
539539
if isinstance(field, marshmallow.fields.TimeDelta):
540540
ret["x-unit"] = field.precision
541+
# Required for Marshmallow <4. Can be removed when support for Marshmallow 3 is dropped.
542+
# This overrides the type set in field2type_and_format (from DEFAULT_FIELD_MAPPING)
543+
if hasattr(field, "serialization_type"):
544+
ret["type"] = {
545+
int: "integer",
546+
float: "number",
547+
}.get(field.serialization_type, "number")
541548
return ret
542549

543550
def enum2properties(self, field, **kwargs: typing.Any) -> dict:

tests/test_ext_marshmallow_field.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import datetime as dt
2+
import importlib.metadata
23
import re
34
from enum import Enum
45

56
import pytest
67
from marshmallow import Schema, fields, validate
8+
from packaging.version import Version
79

810
from .schemas import CategorySchema, CustomIntegerField, CustomList, CustomStringField
911
from .utils import build_ref, get_schemas
1012

13+
MA_VERSION = Version(importlib.metadata.version("marshmallow"))
14+
1115

1216
def test_field2choices_preserving_order(openapi):
1317
choices = ["a", "b", "c", "aa", "0", "cc"]
@@ -28,7 +32,7 @@ def test_field2choices_preserving_order(openapi):
2832
(fields.DateTime, "string"),
2933
(fields.Date, "string"),
3034
(fields.Time, "string"),
31-
(fields.TimeDelta, "integer"),
35+
(fields.TimeDelta, "number" if MA_VERSION.major >= 4 else "integer"),
3236
(fields.Email, "string"),
3337
(fields.URL, "string"),
3438
(fields.IP, "string"),
@@ -45,6 +49,26 @@ def test_field2property_type(FieldClass, jsontype, spec_fixture):
4549
assert res["type"] == jsontype
4650

4751

52+
@pytest.mark.skipif(
53+
MA_VERSION.major >= 4, reason="Marshmallow 4 removed serialization_type attribute"
54+
)
55+
@pytest.mark.parametrize(
56+
("serialization_type", "expected_type"),
57+
[
58+
(int, "integer"),
59+
(float, "number"),
60+
],
61+
)
62+
def test_field2property_type_for_timedelta_marshmallow_3(
63+
spec_fixture,
64+
serialization_type,
65+
expected_type,
66+
):
67+
field = fields.TimeDelta(serialization_type=serialization_type)
68+
res = spec_fixture.openapi.field2property(field)
69+
assert res["type"] == expected_type
70+
71+
4872
def test_field2property_no_type(spec_fixture):
4973
field = fields.Raw()
5074
res = spec_fixture.openapi.field2property(field)

0 commit comments

Comments
 (0)