Repository: springdoc/springdoc-openapi
Pull request #654 (2020-05-07 07:12:45)

Use oneOf schema for polymorphic types
https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/


Patch:
diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocAnnotationsUtils.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocAnnotationsUtils.java
index e7e0dcc3f..e1320b3f4 100644
--- a/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocAnnotationsUtils.java
+++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocAnnotationsUtils.java
@@ -56,7 +56,7 @@ public class SpringDocAnnotationsUtils extends AnnotationsUtils {
 			JsonView jsonView, Annotation[] annotations) {
 		Schema schemaObject = extractSchema(components, schemaImplementation, jsonView, annotations);
 		if (schemaObject != null && StringUtils.isBlank(schemaObject.get$ref())
-				&& StringUtils.isBlank(schemaObject.getType())) {
+				&& StringUtils.isBlank(schemaObject.getType()) && !(schemaObject instanceof ComposedSchema)) {
 			// default to string
 			schemaObject.setType("string");
 		}
diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfiguration.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfiguration.java
index ba4e72a56..9cb480a65 100644
--- a/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfiguration.java
+++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfiguration.java
@@ -35,6 +35,7 @@ import io.swagger.v3.oas.models.media.Schema;
 import org.springdoc.core.converters.AdditionalModelsConverter;
 import org.springdoc.core.converters.FileSupportConverter;
 import org.springdoc.core.converters.ModelConverterRegistrar;
+import org.springdoc.core.converters.PolymorphicModelConverter;
 import org.springdoc.core.converters.PropertyCustomizingConverter;
 import org.springdoc.core.converters.ResponseSupportConverter;
 import org.springdoc.core.converters.SchemaPropertyDeprecatingConverter;
@@ -84,7 +85,7 @@ public class SpringDocConfiguration {
 
 	@Bean
 	@Lazy(false)
-	AdditionalModelsConverter pageableSupportConverter() {
+	AdditionalModelsConverter additionalModelsConverter() {
 		return new AdditionalModelsConverter();
 	}
 
@@ -115,6 +116,13 @@ public class SpringDocConfiguration {
 		return new SchemaPropertyDeprecatingConverter();
 	}
 
+	@Bean
+	@ConditionalOnMissingBean
+	@Lazy(false)
+	PolymorphicModelConverter polymorphicModelConverter() {
+		return new PolymorphicModelConverter();
+	}
+
 	@Bean
 	@ConditionalOnMissingBean
 	OpenAPIBuilder openAPIBuilder(Optional<OpenAPI> openAPI, ApplicationContext context,
diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/converters/PolymorphicModelConverter.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/converters/PolymorphicModelConverter.java
new file mode 100644
index 000000000..84fedce4e
--- /dev/null
+++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/converters/PolymorphicModelConverter.java
@@ -0,0 +1,70 @@
+/*
+ *
+ *  *
+ *  *  * Copyright 2019-2020 the original author or authors.
+ *  *  *
+ *  *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  *  * you may not use this file except in compliance with the License.
+ *  *  * You may obtain a copy of the License at
+ *  *  *
+ *  *  *      https://www.apache.org/licenses/LICENSE-2.0
+ *  *  *
+ *  *  * Unless required by applicable law or agreed to in writing, software
+ *  *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  *  * See the License for the specific language governing permissions and
+ *  *  * limitations under the License.
+ *  *
+ *
+ */
+
+package org.springdoc.core.converters;
+
+import java.lang.reflect.Modifier;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.databind.JavaType;
+import io.swagger.v3.core.converter.AnnotatedType;
+import io.swagger.v3.core.converter.ModelConverter;
+import io.swagger.v3.core.converter.ModelConverterContext;
+import io.swagger.v3.core.util.Json;
+import io.swagger.v3.oas.models.media.ComposedSchema;
+import io.swagger.v3.oas.models.media.Schema;
+
+public class PolymorphicModelConverter implements ModelConverter {
+	@Override
+	public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) {
+		if (chain.hasNext()) {
+			Schema<?> resolvedSchema = chain.next().resolve(type, context, chain);
+			if (resolvedSchema == null || resolvedSchema.get$ref() == null) return resolvedSchema;
+			return composePolymorphicSchema(type, resolvedSchema, context.getDefinedModels().values());
+		}
+		return null;
+	}
+
+	private Schema composePolymorphicSchema(AnnotatedType type, Schema schema, Collection<Schema> schemas) {
+		String ref = schema.get$ref();
+		List<Schema> composedSchemas = schemas.stream()
+				.filter(s -> s instanceof ComposedSchema)
+				.map(s -> (ComposedSchema) s)
+				.filter(s -> s.getAllOf() != null)
+				.filter(s -> s.getAllOf().stream().anyMatch(s2 -> ref.equals(s2.get$ref())))
+				.map(s -> new Schema().$ref("#/components/schemas/" + s.getName()))
+				.collect(Collectors.toList());
+		if (composedSchemas.isEmpty()) return schema;
+
+		ComposedSchema result = new ComposedSchema();
+		if (isConcreteClass(type)) result.addOneOfItem(schema);
+		composedSchemas.forEach(result::addOneOfItem);
+		return result;
+	}
+
+	private boolean isConcreteClass(AnnotatedType type) {
+		JavaType javaType = Json.mapper().constructType(type.getType());
+		Class<?> clazz = javaType.getRawClass();
+		return !Modifier.isAbstract(clazz.getModifiers()) && !clazz.isInterface();
+	}
+}


Test patch:
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app118/AbstractParent.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app118/AbstractParent.java
new file mode 100644
index 000000000..679f24ba4
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app118/AbstractParent.java
@@ -0,0 +1,47 @@
+package test.org.springdoc.api.app118;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
+
+@JsonTypeInfo(use = Id.NAME, property = "type")
+@JsonSubTypes({
+		@Type(ChildOfAbstract1.class),
+		@Type(ChildOfAbstract2.class)
+})
+public abstract class AbstractParent {
+	private int id;
+
+	public int getId() {
+		return id;
+	}
+
+	public void setId(int id) {
+		this.id = id;
+	}
+}
+
+class ChildOfAbstract1 extends AbstractParent {
+	private String abstrachChild1Param;
+
+	public String getAbstrachChild1Param() {
+		return abstrachChild1Param;
+	}
+
+	public void setAbstrachChild1Param(String abstrachChild1Param) {
+		this.abstrachChild1Param = abstrachChild1Param;
+	}
+}
+
+class ChildOfAbstract2 extends AbstractParent {
+	private String abstractChild2Param;
+
+	public String getAbstractChild2Param() {
+		return abstractChild2Param;
+	}
+
+	public void setAbstractChild2Param(String abstractChild2Param) {
+		this.abstractChild2Param = abstractChild2Param;
+	}
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app118/ConcreteParent.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app118/ConcreteParent.java
new file mode 100644
index 000000000..217de4c9f
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app118/ConcreteParent.java
@@ -0,0 +1,47 @@
+package test.org.springdoc.api.app118;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
+
+@JsonTypeInfo(use = Id.NAME, property = "type")
+@JsonSubTypes({
+		@Type(ChildOfConcrete1.class),
+		@Type(ChildOfConcrete2.class)
+})
+public class ConcreteParent {
+	private int id;
+
+	public int getId() {
+		return id;
+	}
+
+	public void setId(int id) {
+		this.id = id;
+	}
+}
+
+class ChildOfConcrete1 extends ConcreteParent {
+	private String concreteChild1Param;
+
+	public String getConcreteChild1Param() {
+		return concreteChild1Param;
+	}
+
+	public void setConcreteChild1Param(String concreteChild1Param) {
+		this.concreteChild1Param = concreteChild1Param;
+	}
+}
+
+class ChildOfConcrete2 extends ConcreteParent {
+	private String concreteChild2Param;
+
+	public String getConcreteChild2Param() {
+		return concreteChild2Param;
+	}
+
+	public void setConcreteChild2Param(String concreteChild2Param) {
+		this.concreteChild2Param = concreteChild2Param;
+	}
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app118/Controller.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app118/Controller.java
new file mode 100644
index 000000000..ed7525b33
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app118/Controller.java
@@ -0,0 +1,44 @@
+package test.org.springdoc.api.app118;
+
+import java.util.List;
+
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("class-hierarchy")
+public class Controller {
+	@PostMapping("abstract-parent")
+	public Response abstractParent(@RequestBody AbstractParent payload) {
+		return null;
+	}
+
+	@PostMapping("concrete-parent")
+	public Response concreteParent(@RequestBody ConcreteParent payload) {
+		return null;
+	}
+}
+
+class Response {
+	AbstractParent abstractParent;
+
+	List<ConcreteParent> concreteParents;
+
+	public AbstractParent getAbstractParent() {
+		return abstractParent;
+	}
+
+	public void setAbstractParent(AbstractParent abstractParent) {
+		this.abstractParent = abstractParent;
+	}
+
+	public List<ConcreteParent> getConcreteParents() {
+		return concreteParents;
+	}
+
+	public void setConcreteParents(List<ConcreteParent> concreteParents) {
+		this.concreteParents = concreteParents;
+	}
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app118/SpringDocApp118Test.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app118/SpringDocApp118Test.java
new file mode 100644
index 000000000..8c12290db
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app118/SpringDocApp118Test.java
@@ -0,0 +1,12 @@
+package test.org.springdoc.api.app118;
+
+import test.org.springdoc.api.AbstractSpringDocTest;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+public class SpringDocApp118Test extends AbstractSpringDocTest {
+
+	@SpringBootApplication
+	static class SpringDocTestApp {}
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/resources/results/app118.json b/springdoc-openapi-webmvc-core/src/test/resources/results/app118.json
new file mode 100644
index 000000000..1f5dfa891
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/resources/results/app118.json
@@ -0,0 +1,227 @@
+{
+  "openapi": "3.0.1",
+  "info": {
+    "title": "OpenAPI definition",
+    "version": "v0"
+  },
+  "servers": [
+    {
+      "url": "http://localhost",
+      "description": "Generated server url"
+    }
+  ],
+  "paths": {
+    "/class-hierarchy/concrete-parent": {
+      "post": {
+        "tags": [
+          "controller"
+        ],
+        "operationId": "concreteParent",
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "oneOf": [
+                  {
+                    "$ref": "#/components/schemas/ConcreteParent"
+                  },
+                  {
+                    "$ref": "#/components/schemas/ChildOfConcrete1"
+                  },
+                  {
+                    "$ref": "#/components/schemas/ChildOfConcrete2"
+                  }
+                ]
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "*/*": {
+                "schema": {
+                  "$ref": "#/components/schemas/Response"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/class-hierarchy/abstract-parent": {
+      "post": {
+        "tags": [
+          "controller"
+        ],
+        "operationId": "abstractParent",
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "oneOf": [
+                  {
+                    "$ref": "#/components/schemas/ChildOfAbstract1"
+                  },
+                  {
+                    "$ref": "#/components/schemas/ChildOfAbstract2"
+                  }
+                ]
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "*/*": {
+                "schema": {
+                  "$ref": "#/components/schemas/Response"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "ChildOfConcrete1": {
+        "type": "object",
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/ConcreteParent"
+          },
+          {
+            "type": "object",
+            "properties": {
+              "concreteChild1Param": {
+                "type": "string"
+              }
+            }
+          }
+        ]
+      },
+      "ChildOfConcrete2": {
+        "type": "object",
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/ConcreteParent"
+          },
+          {
+            "type": "object",
+            "properties": {
+              "concreteChild2Param": {
+                "type": "string"
+              }
+            }
+          }
+        ]
+      },
+      "ConcreteParent": {
+        "required": [
+          "type"
+        ],
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "type": {
+            "type": "string"
+          }
+        },
+        "discriminator": {
+          "propertyName": "type"
+        }
+      },
+      "AbstractParent": {
+        "required": [
+          "type"
+        ],
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "type": {
+            "type": "string"
+          }
+        },
+        "discriminator": {
+          "propertyName": "type"
+        }
+      },
+      "ChildOfAbstract1": {
+        "type": "object",
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/AbstractParent"
+          },
+          {
+            "type": "object",
+            "properties": {
+              "abstrachChild1Param": {
+                "type": "string"
+              }
+            }
+          }
+        ]
+      },
+      "ChildOfAbstract2": {
+        "type": "object",
+        "allOf": [
+          {
+            "$ref": "#/components/schemas/AbstractParent"
+          },
+          {
+            "type": "object",
+            "properties": {
+              "abstractChild2Param": {
+                "type": "string"
+              }
+            }
+          }
+        ]
+      },
+      "Response": {
+        "type": "object",
+        "properties": {
+          "abstractParent": {
+            "oneOf": [
+              {
+                "$ref": "#/components/schemas/ChildOfAbstract1"
+              },
+              {
+                "$ref": "#/components/schemas/ChildOfAbstract2"
+              }
+            ]
+          },
+          "concreteParents": {
+            "type": "array",
+            "items": {
+              "oneOf": [
+                {
+                  "$ref": "#/components/schemas/ConcreteParent"
+                },
+                {
+                  "$ref": "#/components/schemas/ChildOfConcrete1"
+                },
+                {
+                  "$ref": "#/components/schemas/ChildOfConcrete2"
+                }
+              ]
+            }
+          }
+        }
+      }
+    }
+  }
+}
\ No newline at end of file