Repository: marshmallow-code/apispec
Pull request #701 (2021-07-28 18:45:57)

MarshmallowPlugin: support allOf, oneOf, anyOf, not
Ripped-off and replaces #391.

Thanks @stefanv for the initial work on this and sorry about the delay.


Patch:
diff --git a/src/apispec/ext/marshmallow/field_converter.py b/src/apispec/ext/marshmallow/field_converter.py
index a579682..3bad7d3 100644
--- a/src/apispec/ext/marshmallow/field_converter.py
+++ b/src/apispec/ext/marshmallow/field_converter.py
@@ -65,6 +65,9 @@ _VALID_PROPERTIES = {
     "type",
     "items",
     "allOf",
+    "oneOf",
+    "anyOf",
+    "not",
     "properties",
     "additionalProperties",
     "readOnly",
diff --git a/src/apispec/ext/marshmallow/schema_resolver.py b/src/apispec/ext/marshmallow/schema_resolver.py
index 407376f..5c6afb7 100644
--- a/src/apispec/ext/marshmallow/schema_resolver.py
+++ b/src/apispec/ext/marshmallow/schema_resolver.py
@@ -282,6 +282,13 @@ class SchemaResolver:
                     k: self.resolve_schema_dict(v)
                     for k, v in schema["properties"].items()
                 }
+            for keyword in ("oneOf", "anyOf", "allOf"):
+                if keyword in schema:
+                    schema[keyword] = [
+                        self.resolve_schema_dict(s) for s in schema[keyword]
+                    ]
+            if "not" in schema:
+                schema["not"] = self.resolve_schema_dict(schema["not"])
             return schema
 
         return self.converter.resolve_nested_schema(schema)


Test patch:
diff --git a/tests/test_ext_marshmallow.py b/tests/test_ext_marshmallow.py
index ba1a11f..8477ba6 100644
--- a/tests/test_ext_marshmallow.py
+++ b/tests/test_ext_marshmallow.py
@@ -11,6 +11,7 @@ from apispec.ext.marshmallow import common
 from apispec.exceptions import APISpecError
 from .schemas import (
     PetSchema,
+    SampleSchema,
     AnalysisSchema,
     RunSchema,
     SelfReferencingSchema,
@@ -673,6 +674,88 @@ class TestOperationHelper:
             "schema"
         ] == build_ref(spec, "schema", "Pet")
 
+    @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
+    def test_schema_resolver_allof_v2(self, spec_fixture):
+        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
+        spec_fixture.spec.components.schema("Sample", schema=SampleSchema)
+        spec_fixture.spec.path(
+            path="/pet",
+            operations={
+                "get": {
+                    "responses": {200: {"schema": {"allOf": [PetSchema, SampleSchema]}}}
+                }
+            },
+        )
+        get = get_paths(spec_fixture.spec)["/pet"]["get"]
+        assert get["responses"]["200"]["schema"] == {
+            "allOf": [
+                build_ref(spec_fixture.spec, "schema", "Pet"),
+                build_ref(spec_fixture.spec, "schema", "Sample"),
+            ]
+        }
+
+    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
+    @pytest.mark.parametrize("combinator", ["oneOf", "anyOf", "allOf"])
+    def test_schema_resolver_oneof_anyof_allof_v3(self, spec_fixture, combinator):
+        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
+        spec_fixture.spec.path(
+            path="/pet",
+            operations={
+                "get": {
+                    "responses": {
+                        200: {
+                            "content": {
+                                "application/json": {
+                                    "schema": {combinator: [PetSchema, SampleSchema]}
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+        )
+        get = get_paths(spec_fixture.spec)["/pet"]["get"]
+        assert get["responses"]["200"]["content"]["application/json"]["schema"] == {
+            combinator: [
+                build_ref(spec_fixture.spec, "schema", "Pet"),
+                build_ref(spec_fixture.spec, "schema", "Sample"),
+            ]
+        }
+
+    @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
+    def test_schema_resolver_not_v2(self, spec_fixture):
+        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
+        spec_fixture.spec.path(
+            path="/pet",
+            operations={"get": {"responses": {200: {"schema": {"not": PetSchema}}}}},
+        )
+        get = get_paths(spec_fixture.spec)["/pet"]["get"]
+        assert get["responses"]["200"]["schema"] == {
+            "not": build_ref(spec_fixture.spec, "schema", "Pet"),
+        }
+
+    @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
+    def test_schema_resolver_not_v3(self, spec_fixture):
+        spec_fixture.spec.components.schema("Pet", schema=PetSchema)
+        spec_fixture.spec.path(
+            path="/pet",
+            operations={
+                "get": {
+                    "responses": {
+                        200: {
+                            "content": {
+                                "application/json": {"schema": {"not": PetSchema}}
+                            }
+                        }
+                    }
+                }
+            },
+        )
+        get = get_paths(spec_fixture.spec)["/pet"]["get"]
+        assert get["responses"]["200"]["content"]["application/json"]["schema"] == {
+            "not": build_ref(spec_fixture.spec, "schema", "Pet"),
+        }
+
     @pytest.mark.parametrize(
         "pet_schema",
         (PetSchema, PetSchema(), "tests.schemas.PetSchema"),