Repository: marshmallow-code/apispec
Pull request #889 (2024-02-26 19:52:27)

Fix nullable enums


Patch:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index c3082a8..77d7c52 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -80,3 +80,4 @@ Contributors (chronological)
 - Tayler Sokalski `@tsokalski <https://github.com/tsokalski>`_
 - Sebastien Lovergne `@TheBigRoomXXL <https://github.com/TheBigRoomXXL>`_
 - Luna Lovegood `@duchuyvp <https://github.com/duchuyvp>`_
+- Tobias Kolditz `@kolditz-senec <https://github.com/kolditz-senec>`_
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index fb3f4f4..9ed68da 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -4,6 +4,12 @@ Changelog
 6.5.0 (unreleased)
 ******************
 
+Bug fixes:
+
+- Include ``null`` as a value when using ``validate.OneOf`` or ``fields.Enum``
+  when ``allow_none`` is ``True`` for a field (:issue:`812`).
+  Thanks :user:`pmdarrow` for reporting and :user:`kolditz-senec` for the PR.
+
 Other changes:
 
 - Deprecate the ``__version__`` attribute. Use feature detection, or
diff --git a/src/apispec/ext/marshmallow/field_converter.py b/src/apispec/ext/marshmallow/field_converter.py
index 3984851..8411383 100644
--- a/src/apispec/ext/marshmallow/field_converter.py
+++ b/src/apispec/ext/marshmallow/field_converter.py
@@ -253,6 +253,11 @@ class FieldConverterMixin:
             if choices:
                 attributes["enum"] = list(functools.reduce(operator.and_, choices))
 
+        if field.allow_none:
+            enum = attributes.get("enum")
+            if enum is not None and None not in enum:
+                attributes["enum"].append(None)
+
         return attributes
 
     def field2read_only(
@@ -517,6 +522,8 @@ class FieldConverterMixin:
             else:
                 choices = (m.value for m in field.enum)
             ret["enum"] = [field.field._serialize(v, None, None) for v in choices]
+            if field.allow_none and None not in ret["enum"]:
+                ret["enum"].append(None)
         return ret
 
     def datetime2properties(self, field, **kwargs: typing.Any) -> dict:


Test patch:
diff --git a/tests/test_ext_marshmallow_field.py b/tests/test_ext_marshmallow_field.py
index 6239644..b4f7362 100644
--- a/tests/test_ext_marshmallow_field.py
+++ b/tests/test_ext_marshmallow_field.py
@@ -122,6 +122,22 @@ def test_field_with_choices(spec_fixture):
     assert set(res["enum"]) == {"freddie", "brian", "john"}
 
 
+def test_field_with_nullable_choices(spec_fixture):
+    field = fields.Str(
+        validate=validate.OneOf(["freddie", "brian", "john"]), allow_none=True
+    )
+    res = spec_fixture.openapi.field2property(field)
+    assert set(res["enum"]) == {"freddie", "brian", "john", None}
+
+
+def test_field_with_nullable_choices_returns_only_one_none(spec_fixture):
+    field = fields.Str(
+        validate=validate.OneOf(["freddie", "brian", "john", None]), allow_none=True
+    )
+    res = spec_fixture.openapi.field2property(field)
+    assert res["enum"] == ["freddie", "brian", "john", None]
+
+
 def test_field_with_equal(spec_fixture):
     field = fields.Str(validate=validate.Equal("only choice"))
     res = spec_fixture.openapi.field2property(field)
@@ -305,6 +321,27 @@ def test_enum_value_field(spec_fixture, by_value):
     assert ret["enum"] == [1, 2]
 
 
+def test_nullable_enum(spec_fixture):
+    class MyEnum(Enum):
+        one = 1
+        two = 2
+
+    field = fields.Enum(MyEnum, allow_none=True, by_value=True)
+    ret = spec_fixture.openapi.field2property(field)
+    assert ret["enum"] == [1, 2, None]
+
+
+def test_nullable_enum_returns_only_one_none(spec_fixture):
+    class MyEnum(Enum):
+        one = 1
+        two = 2
+        three = None
+
+    field = fields.Enum(MyEnum, allow_none=True, by_value=True)
+    ret = spec_fixture.openapi.field2property(field)
+    assert ret["enum"] == [1, 2, None]
+
+
 def test_field2property_nested_spec_metadatas(spec_fixture):
     spec_fixture.spec.components.schema("Category", schema=CategorySchema)
     category = fields.Nested(