Skip to content

Commit 5220bdf

Browse files
committed
fix(marshmallow): serialize OneOf and Equal validator choices to OpenAPI enum (fixes #679)
1 parent 0b66a26 commit 5220bdf

2 files changed

Lines changed: 77 additions & 9 deletions

File tree

‎src/apispec/ext/marshmallow/field_converter.py‎

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,21 @@ def field2default(
232232
ret["default"] = default
233233
return ret
234234

235+
def _serialize_choice(
236+
self, field: marshmallow.fields.Field, choice: typing.Any
237+
) -> typing.Any:
238+
if choice is None:
239+
return None
240+
target_field = (
241+
field.inner if isinstance(field, marshmallow.fields.List) else field
242+
)
243+
try:
244+
val = target_field._serialize(choice, None, None)
245+
hash(val)
246+
return val
247+
except Exception:
248+
return choice
249+
235250
def field2choices(
236251
self, field: marshmallow.fields.Field, **kwargs: typing.Any
237252
) -> dict:
@@ -245,9 +260,15 @@ def field2choices(
245260
choices = []
246261
for validator in field.validators:
247262
if hasattr(validator, "choices"):
248-
choices.append(OrderedSet(validator.choices))
263+
choices.append(
264+
OrderedSet(
265+
self._serialize_choice(field, c) for c in validator.choices
266+
)
267+
)
249268
elif hasattr(validator, "comparable"):
250-
choices.append(OrderedSet([validator.comparable]))
269+
choices.append(
270+
OrderedSet([self._serialize_choice(field, validator.comparable)])
271+
)
251272
if choices:
252273
attributes["enum"] = list(functools.reduce(operator.and_, choices))
253274

@@ -550,13 +571,14 @@ def enum2properties(self, field, **kwargs: typing.Any) -> dict:
550571
ret = {}
551572
if isinstance(field, marshmallow.fields.Enum):
552573
ret = self.field2property(field.field)
553-
if field.by_value is False:
554-
choices = (m for m in field.enum.__members__)
555-
else:
556-
choices = (m.value for m in field.enum)
557-
ret["enum"] = [field.field._serialize(v, None, None) for v in choices]
558-
if field.allow_none and None not in ret["enum"]:
559-
ret["enum"].append(None)
574+
if "enum" not in kwargs.get("ret", {}):
575+
if field.by_value is False:
576+
choices = (m for m in field.enum.__members__)
577+
else:
578+
choices = (m.value for m in field.enum)
579+
ret["enum"] = [field.field._serialize(v, None, None) for v in choices]
580+
if field.allow_none and None not in ret["enum"]:
581+
ret["enum"].append(None)
560582
return ret
561583

562584
def datetime2properties(self, field, **kwargs: typing.Any) -> dict:

‎tests/test_ext_marshmallow_field.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime as dt
22
import importlib.metadata
33
import re
4+
import uuid
45
from enum import Enum
56

67
import pytest
@@ -19,6 +20,51 @@ def test_field2choices_preserving_order(openapi):
1920
assert openapi.field2choices(field) == {"enum": choices}
2021

2122

23+
def test_field2choices_serializes_choices(openapi):
24+
date_choices = [dt.date(2023, 1, 1), dt.date(2023, 1, 2)]
25+
field = fields.Date(validate=validate.OneOf(date_choices))
26+
assert openapi.field2choices(field) == {"enum": ["2023-01-01", "2023-01-02"]}
27+
28+
dtime_choices = [dt.datetime(2023, 1, 1, 12, 0)]
29+
field = fields.DateTime(validate=validate.OneOf(dtime_choices))
30+
assert openapi.field2choices(field) == {"enum": ["2023-01-01T12:00:00"]}
31+
32+
u = uuid.uuid4()
33+
field = fields.UUID(validate=validate.OneOf([u]))
34+
assert openapi.field2choices(field) == {"enum": [str(u)]}
35+
36+
field = fields.Date(validate=validate.Equal(dt.date(2023, 1, 1)))
37+
assert openapi.field2choices(field) == {"enum": ["2023-01-01"]}
38+
39+
40+
def test_field2choices_with_labels(openapi):
41+
field = fields.Integer(validate=validate.OneOf([1, 2], labels=["one", "two"]))
42+
assert openapi.field2choices(field) == {"enum": [1, 2]}
43+
44+
45+
def test_enum_field_with_oneof_validator(spec_fixture):
46+
class MyEnum(Enum):
47+
one = 1
48+
two = 2
49+
three = 3
50+
51+
field = fields.Enum(MyEnum, validate=validate.OneOf([MyEnum.one, MyEnum.two]))
52+
ret = spec_fixture.openapi.field2property(field)
53+
assert ret["type"] == "string"
54+
assert ret["enum"] == ["one", "two"]
55+
56+
field_val = fields.Enum(
57+
MyEnum, by_value=True, validate=validate.OneOf([MyEnum.one, MyEnum.two])
58+
)
59+
ret_val = spec_fixture.openapi.field2property(field_val)
60+
assert ret_val["enum"] == [1, 2]
61+
62+
field_eq = fields.Enum(MyEnum, validate=validate.Equal(MyEnum.two))
63+
ret_eq = spec_fixture.openapi.field2property(field_eq)
64+
assert ret_eq["type"] == "string"
65+
assert ret_eq["enum"] == ["two"]
66+
67+
2268
@pytest.mark.parametrize(
2369
("FieldClass", "jsontype"),
2470
[

0 commit comments

Comments
 (0)