Repository: marshmallow-code/apispec

Issue:
Nullable in OAS 3.1 rendered as "'null'" string instead of "null"
Nullable fields should be rendered as `"type": ["fieldType", "null"]`, not `["fieldType", "'null'"]`.

The field converter currently includes quotes around `null` _in the literal string value_, see https://github.com/marshmallow-code/apispec/blob/5cf763e04df42afddc2480991c878da928558417/src/apispec/ext/marshmallow/field_converter.py#L283

which results in JSON like this:

```json
          "created_at": {
            "type": [
              "string",
              "'null'"
            ],
            "format": "date-time",
            "readOnly": true
          },
```

or as YAML:

```yaml
       created_at:
          type:
            - string
            - '''null'''
          format: date-time
          readOnly: true
```

While the type string should be a string (and not `None` in Python, `null` in JSON or YAML), the extra quotes there are an error.

To fix, please drop the extra quotes in the value:

```python
                attributes["type"] = make_type_list(ret.get("type")) + ["null"] 
```

Also see 
- [Understanding JSON Schema: null](https://json-schema.org/understanding-json-schema/reference/null.html)
- this specific [valid specification test](https://github.com/OAI/OpenAPI-Specification/blob/54edcf039c9854dcbc4feac1b1990c21972f3ca5/tests/v3.1/pass/mega.yaml#L45) in the OpenAPI specification repository.

Comments:

Patch:
diff --git a/setup.py b/setup.py
index 527a956..77301e0 100644
--- a/setup.py
+++ b/setup.py
@@ -8,7 +8,7 @@ EXTRAS_REQUIRE = {
     "docs": [
         "marshmallow>=3.0.0",
         "pyyaml==5.4.1",
-        "sphinx==4.0.2",
+        "sphinx==4.0.3",
         "sphinx-issues==1.2.0",
         "sphinx-rtd-theme==0.5.2",
     ],
diff --git a/src/apispec/ext/marshmallow/field_converter.py b/src/apispec/ext/marshmallow/field_converter.py
index c1304a9..7db713a 100644
--- a/src/apispec/ext/marshmallow/field_converter.py
+++ b/src/apispec/ext/marshmallow/field_converter.py
@@ -280,7 +280,7 @@ class FieldConverterMixin:
             elif self.openapi_version.minor < 1:
                 attributes["nullable"] = True
             else:
-                attributes["type"] = make_type_list(ret.get("type")) + ["'null'"]
+                attributes["type"] = [*make_type_list(ret.get("type")), "null"]
         return attributes
 
     def field2range(self, field, ret):


Test patch:
diff --git a/tests/test_ext_marshmallow_field.py b/tests/test_ext_marshmallow_field.py
index 04f70f1..3569e04 100644
--- a/tests/test_ext_marshmallow_field.py
+++ b/tests/test_ext_marshmallow_field.py
@@ -173,7 +173,7 @@ def test_field_with_allow_none(spec_fixture):
         assert res["nullable"] is True
     else:
         assert "nullable" not in res
-        assert res["type"] == ["string", "'null'"]
+        assert res["type"] == ["string", "null"]
 
 
 def test_field_with_dump_only(spec_fixture):
@@ -211,7 +211,7 @@ def test_field_with_range_string_type(spec_fixture, field):
 
 @pytest.mark.parametrize("spec_fixture", ("3.1.0",), indirect=True)
 def test_field_with_range_type_list_with_number(spec_fixture):
-    @spec_fixture.openapi.map_to_openapi_type(["integer", "'null'"], None)
+    @spec_fixture.openapi.map_to_openapi_type(["integer", "null"], None)
     class NullableInteger(fields.Field):
         """Nullable integer"""
 
@@ -219,12 +219,12 @@ def test_field_with_range_type_list_with_number(spec_fixture):
     res = spec_fixture.openapi.field2property(field)
     assert res["minimum"] == 1
     assert res["maximum"] == 10
-    assert res["type"] == ["integer", "'null'"]
+    assert res["type"] == ["integer", "null"]
 
 
 @pytest.mark.parametrize("spec_fixture", ("3.1.0",), indirect=True)
 def test_field_with_range_type_list_without_number(spec_fixture):
-    @spec_fixture.openapi.map_to_openapi_type(["string", "'null'"], None)
+    @spec_fixture.openapi.map_to_openapi_type(["string", "null"], None)
     class NullableInteger(fields.Field):
         """Nullable integer"""
 
@@ -232,7 +232,7 @@ def test_field_with_range_type_list_without_number(spec_fixture):
     res = spec_fixture.openapi.field2property(field)
     assert res["x-minimum"] == 1
     assert res["x-maximum"] == 10
-    assert res["type"] == ["string", "'null'"]
+    assert res["type"] == ["string", "null"]
 
 
 def test_field_with_str_regex(spec_fixture):