Repository: marshmallow-code/apispec

Issue:
Nullable `Raw` fields are not handled well for OAS 3.1.0
When converting a nullable `Raw` Marshmallow field into its corresponding OpenAPI representation for OAS 3.1.0, the generated representation does not match the behaviour of the actual schema/field:

```python
import jsonschema
from marshmallow import Schema, fields

from apispec import APISpec
from apispec.ext.marshmallow import OpenAPIConverter


class TestSchema(Schema):
    raw_field = fields.Raw(allow_none=True)


spec = APISpec(title="Pets", version="0.1", openapi_version="3.1.0")
converter = OpenAPIConverter(
    openapi_version="3.1.0", schema_name_resolver=None, spec=spec
)
json_schema = converter.schema2jsonschema(TestSchema)
data = {"raw_field": "test"}
TestSchema().load(data)  # Works, as expected
jsonschema.validate(data, json_schema)  # Fails unexpectedly
```

The output of printing the `json_schema` variable above is the following:
```python
{'type': 'object', 'properties': {'raw_field': {'type': ['null']}}}
```

It appears the `FieldConverterMixin`'s `field2nullable` is appending `null` to the list of supported types for OAS 3.1.0, which makes sense in many cases, but when there's no pre-existing `type` value defined, the result is just `['null']`, which means that the field can only be `null`. This doesn't match the behaviour of the defined schema.

I've implemented a workaround for this where I've overridden the `DEFAULT_FIELD_MAPPING` for `marshmallow.fields.Raw` to map to all supported JSON types (`["boolean", "object", "array", "number", "string"]`), but it's a bit of a hack, and would be nice if the library could support this a bit better.

Discussion:
tsokalski: Bump @lafrech - any additional feedback on this?

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 60c8bb1..5cd1319 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,9 +1,14 @@
 Changelog
 ---------
 
-6.9.0 (unreleased)
+6.8.1 (unreleased)
 ******************
 
+Bug fixes:
+
+- Fix handling of nullable Raw fields for OAS 3.1.0 (:issue:`960`).
+  Thanks :user:`tsokalski` for reporting and fixing.
+
 Support:
 
 - Support marshmallow 4 (:pr:`963`).
diff --git a/src/apispec/ext/marshmallow/field_converter.py b/src/apispec/ext/marshmallow/field_converter.py
index 3b837b4..9016e5f 100644
--- a/src/apispec/ext/marshmallow/field_converter.py
+++ b/src/apispec/ext/marshmallow/field_converter.py
@@ -318,7 +318,7 @@ class FieldConverterMixin:
                     attributes["anyOf"] = [{"$ref": ret.pop("$ref")}, {"type": "null"}]
                 elif "allOf" in ret:
                     attributes["anyOf"] = [*ret.pop("allOf"), {"type": "null"}]
-                else:
+                elif "type" in ret:
                     attributes["type"] = [*make_type_list(ret.get("type")), "null"]
         return attributes
 


Test patch:
diff --git a/tests/test_ext_marshmallow_field.py b/tests/test_ext_marshmallow_field.py
index ab5cd62..5beba1a 100644
--- a/tests/test_ext_marshmallow_field.py
+++ b/tests/test_ext_marshmallow_field.py
@@ -197,6 +197,20 @@ def test_field_with_allow_none(spec_fixture):
         assert res["type"] == ["string", "null"]
 
 
+@pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True)
+@pytest.mark.parametrize("field_class", [fields.Field, fields.Raw])
+def test_nullable_field_with_no_type(spec_fixture, field_class):
+    field = field_class(allow_none=True)
+    res = spec_fixture.openapi.field2property(field)
+    if spec_fixture.openapi.openapi_version.major < 3:
+        assert res["x-nullable"] is True
+    elif spec_fixture.openapi.openapi_version.minor < 1:
+        assert res["nullable"] is True
+    else:
+        assert "nullable" not in res
+        assert "type" not in res
+
+
 @pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True)
 def test_nested_nullable(spec_fixture):
     class Child(Schema):