Repository: marshmallow-code/apispec
Pull request #959 (2024-12-02 20:44:36)

[pre-commit.ci] pre-commit autoupdate
<!--pre-commit.ci start-->
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.2 → v0.8.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.2...v0.8.1)
- [github.com/python-jsonschema/check-jsonschema: 0.29.4 → 0.30.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.29.4...0.30.0)
<!--pre-commit.ci end-->


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d8a1c26..e3c1fde 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,12 +2,12 @@ ci:
   autoupdate_schedule: monthly
 repos:
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.7.2
+  rev: v0.8.1
   hooks:
     - id: ruff
     - id: ruff-format
 - repo: https://github.com/python-jsonschema/check-jsonschema
-  rev: 0.29.4
+  rev: 0.30.0
   hooks:
     - id: check-github-workflows
     - id: check-readthedocs
diff --git a/src/apispec/ext/marshmallow/field_converter.py b/src/apispec/ext/marshmallow/field_converter.py
index 63fbe96..3b837b4 100644
--- a/src/apispec/ext/marshmallow/field_converter.py
+++ b/src/apispec/ext/marshmallow/field_converter.py
@@ -306,6 +306,11 @@ class FieldConverterMixin:
                         {"type": "object", "nullable": True},
                         {"$ref": ret.pop("$ref")},
                     ]
+                elif "allOf" in ret:
+                    attributes["anyOf"] = [
+                        *ret.pop("allOf"),
+                        {"type": "object", "nullable": True},
+                    ]
                 else:
                     attributes["nullable"] = True
             else:
@@ -460,7 +465,17 @@ class FieldConverterMixin:
             field, marshmallow.fields.Pluck
         ):
             schema_dict = self.resolve_nested_schema(field.schema)  # type:ignore
-            if ret and "$ref" in schema_dict:
+            if (
+                ret
+                and "$ref" in schema_dict
+                and (
+                    self.openapi_version.major < 3
+                    or (
+                        self.openapi_version.major == 3
+                        and self.openapi_version.minor == 0
+                    )
+                )
+            ):
                 ret.update({"allOf": [schema_dict]})
             else:
                 ret.update(schema_dict)


Test patch:
diff --git a/tests/test_ext_marshmallow_field.py b/tests/test_ext_marshmallow_field.py
index 5e5250b..36d864c 100644
--- a/tests/test_ext_marshmallow_field.py
+++ b/tests/test_ext_marshmallow_field.py
@@ -222,6 +222,42 @@ def test_nested_nullable(spec_fixture):
         }
 
 
+@pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True)
+def test_nested_nullable_with_metadata(spec_fixture):
+    # Regression test for https://github.com/marshmallow-code/apispec/issues/955
+    class Child(Schema):
+        name = fields.Str()
+
+    field = fields.Nested(
+        Child,
+        allow_none=True,
+        metadata={"description": "foo"},
+    )
+    res = spec_fixture.openapi.field2property(field)
+    version = spec_fixture.openapi.openapi_version
+    if version.major < 3:
+        assert res == {
+            "allOf": [
+                {"$ref": "#/definitions/Child"},
+            ],
+            "x-nullable": True,
+            "description": "foo",
+        }
+    elif version.major == 3 and version.minor < 1:
+        assert res == {
+            "anyOf": [
+                {"$ref": "#/components/schemas/Child"},
+                {"type": "object", "nullable": True},
+            ],
+            "description": "foo",
+        }
+    else:
+        assert res == {
+            "anyOf": [{"$ref": "#/components/schemas/Child"}, {"type": "null"}],
+            "description": "foo",
+        }
+
+
 @pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True)
 def test_nullable_pluck(spec_fixture):
     class Example(Schema):
@@ -387,10 +423,13 @@ def test_nullable_enum_returns_only_one_none(spec_fixture):
     assert ret["enum"] == [1, 2, None]
 
 
+@pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True)
 def test_field2property_nested_spec_metadatas(spec_fixture):
-    spec_fixture.spec.components.schema("Category", schema=CategorySchema)
+    class Child(Schema):
+        name = fields.Str()
+
     category = fields.Nested(
-        CategorySchema,
+        Child,
         metadata={
             "description": "A category",
             "invalid_property": "not in the result",
@@ -398,11 +437,19 @@ def test_field2property_nested_spec_metadatas(spec_fixture):
         },
     )
     result = spec_fixture.openapi.field2property(category)
-    assert result == {
-        "allOf": [build_ref(spec_fixture.spec, "schema", "Category")],
-        "description": "A category",
-        "x-extension": "A great extension",
-    }
+    version = spec_fixture.openapi.openapi_version
+    if version.major < 3 or version.minor < 1:
+        assert result == {
+            "allOf": [build_ref(spec_fixture.spec, "schema", "Child")],
+            "description": "A category",
+            "x-extension": "A great extension",
+        }
+    else:
+        assert result == {
+            **build_ref(spec_fixture.spec, "schema", "Child"),
+            "description": "A category",
+            "x-extension": "A great extension",
+        }
 
 
 def test_field2property_nested_spec(spec_fixture):