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 static Schema resolveSchemaFromType(Class schemaImplementation, Compon 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 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 @@ LocalVariableTableParameterNameDiscoverer localSpringDocParameterNameDiscoverer( @Bean @Lazy(false) - AdditionalModelsConverter pageableSupportConverter() { + AdditionalModelsConverter additionalModelsConverter() { return new AdditionalModelsConverter(); } @@ -115,6 +116,13 @@ SchemaPropertyDeprecatingConverter schemaPropertyDeprecatingConverter() { return new SchemaPropertyDeprecatingConverter(); } + @Bean + @ConditionalOnMissingBean + @Lazy(false) + PolymorphicModelConverter polymorphicModelConverter() { + return new PolymorphicModelConverter(); + } + @Bean @ConditionalOnMissingBean OpenAPIBuilder openAPIBuilder(Optional 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 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 schemas) { + String ref = schema.get$ref(); + List 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(); + } +} 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 concreteParents; + + public AbstractParent getAbstractParent() { + return abstractParent; + } + + public void setAbstractParent(AbstractParent abstractParent) { + this.abstractParent = abstractParent; + } + + public List getConcreteParents() { + return concreteParents; + } + + public void setConcreteParents(List 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