Repository: marshmallow-code/apispec
Pull request #958 (2024-11-23 07:26:55)

Allow properties on $ref objects for OpenAPI 3.1.
JSON Schema draft 5 (OpenAPI 3.0.4), it specifically says additional properties on a $ref object are ignored.

> All other properties in a "$ref" object MUST be ignored.

https://datatracker.ietf.org/doc/html/draft-wright-json-schema-00#section-7

However, draft 2020-12 (OpenAPI 3.1.0) seems to have changed its tune:

> Its results are the results of the referenced schema.  [[CREF5: Note
> that this definition of how the results are determined means that other
> keywords can appear alongside of "$ref" in the same schema object.  ]]

https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-00#section-8.2.3.1

So if I'm understanding correctly, things like this are now valid and meaningful:

```json
{
            "$ref": "#/components/schemas/Child",
            "description": "A category",
            "x-extension": "A great extension",
 }
```


Patch:
diff --git a/src/apispec/ext/marshmallow/field_converter.py b/src/apispec/ext/marshmallow/field_converter.py
index 63fbe96..5ea40f6 100644
--- a/src/apispec/ext/marshmallow/field_converter.py
+++ b/src/apispec/ext/marshmallow/field_converter.py
@@ -460,7 +460,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..837cf0d 100644
--- a/tests/test_ext_marshmallow_field.py
+++ b/tests/test_ext_marshmallow_field.py
@@ -387,10 +387,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 +401,29 @@ 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:
+        assert result == {
+            "allOf": [
+                {"$ref": "#/definitions/Child"},
+            ],
+            "description": "A category",
+            "x-extension": "A great extension",
+        }
+    elif version.minor < 1:
+        assert result == {
+            "allOf": [
+                {"$ref": "#/components/schemas/Child"},
+            ],
+            "description": "A category",
+            "x-extension": "A great extension",
+        }
+    else:
+        assert result == {
+            "$ref": "#/components/schemas/Child",
+            "description": "A category",
+            "x-extension": "A great extension",
+        }
 
 
 def test_field2property_nested_spec(spec_fixture):