diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/PolymorphicModelConverter.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/PolymorphicModelConverter.java index 95a93340d..eb9e04adb 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/PolymorphicModelConverter.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/PolymorphicModelConverter.java @@ -122,7 +122,7 @@ private List findComposedSchemas(String ref, Collection schemas) .filter(s -> s.getAllOf() != null) .filter(s -> s.getAllOf().stream().anyMatch(s2 -> ref.equals(s2.get$ref()))) .map(s -> new Schema().$ref(AnnotationsUtils.COMPONENTS_REF + s.getName())) - .collect(Collectors.toList()); + .toList(); List resultSchemas = new ArrayList<>(composedSchemas); diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocAnnotationsUtils.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocAnnotationsUtils.java index c532a679a..0c3750eea 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocAnnotationsUtils.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocAnnotationsUtils.java @@ -31,9 +31,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonView; @@ -65,6 +67,7 @@ /** * The type Spring doc annotations utils. + * * @author bnasslahsen */ @SuppressWarnings({ "rawtypes" }) @@ -142,10 +145,21 @@ public static Schema extractSchema(Components components, Type returnType, JsonV for (Map.Entry entry : schemaMap.entrySet()) { // If we've seen this schema before but find later it should be polymorphic, // replace the existing schema with this richer version. + Schema existingSchema = componentSchemas.get(entry.getKey()); if (!componentSchemas.containsKey(entry.getKey()) || - (!entry.getValue().getClass().equals(componentSchemas.get(entry.getKey()).getClass()) && entry.getValue().getAllOf() != null)) { + (!entry.getValue().getClass().equals(existingSchema.getClass()) && entry.getValue().getAllOf() != null)) { componentSchemas.put(entry.getKey(), entry.getValue()); } + else if (componentSchemas.containsKey(entry.getKey()) && schemaMap.containsKey(entry.getKey())) { + // Check to merge polymorphic types + Set existingAllOf = new LinkedHashSet<>(); + if(existingSchema.getAllOf() != null) + existingAllOf.addAll(existingSchema.getAllOf()); + if (schemaMap.get(entry.getKey()).getAllOf() != null){ + existingAllOf.addAll(schemaMap.get(entry.getKey()).getAllOf()); + existingSchema.setAllOf(new ArrayList<>(existingAllOf)); + } + } } components.setSchemas(componentSchemas); } @@ -207,8 +221,8 @@ public static Optional getContent(io.swagger.v3.oas.annotations.media.C * Merge schema. * * @param existingContent the existing content - * @param schemaN the schema n - * @param mediaTypeStr the media type str + * @param schemaN the schema n + * @param mediaTypeStr the media type str */ public static void mergeSchema(Content existingContent, Schema schemaN, String mediaTypeStr) { if (existingContent.containsKey(mediaTypeStr)) { @@ -322,7 +336,7 @@ private static void addExtension(io.swagger.v3.oas.annotations.media.Content ann * Sets examples. * * @param mediaType the media type - * @param examples the examples + * @param examples the examples */ private static void setExamples(MediaType mediaType, ExampleObject[] examples) { if (examples.length == 1 && StringUtils.isBlank(examples[0].name())) { @@ -436,7 +450,7 @@ private static boolean isArray(io.swagger.v3.oas.annotations.media.Content annot * Resolve default value object. * * @param defaultValueStr the default value str - * @param objectMapper the object mapper + * @param objectMapper the object mapper * @return the object */ public static Object resolveDefaultValue(String defaultValueStr, ObjectMapper objectMapper) { diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app222/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app222/HelloController.java new file mode 100644 index 000000000..0415f7911 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app222/HelloController.java @@ -0,0 +1,28 @@ +package test.org.springdoc.api.v30.app222; + + + +import test.org.springdoc.api.v30.app222.SpringDocApp222Test.FirstHierarchyUser; +import test.org.springdoc.api.v30.app222.SpringDocApp222Test.SecondHierarchyUser; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author bnasslahsen + */ + +@RestController +class HelloController { + + @GetMapping("/hello1") + public FirstHierarchyUser getItems1() { + return null; + } + + @GetMapping("/hello2") + public SecondHierarchyUser getItems2() { + return null; + } + +} \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app222/SpringDocApp222Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app222/SpringDocApp222Test.java new file mode 100644 index 000000000..d25dc5598 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app222/SpringDocApp222Test.java @@ -0,0 +1,55 @@ +/* + * + * * + * * * + * * * * + * * * * * Copyright 2019-2022 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 test.org.springdoc.api.v30.app222; + +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; +import test.org.springdoc.api.v30.AbstractSpringDocV30Test; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +public class SpringDocApp222Test extends AbstractSpringDocV30Test { + + @SpringBootApplication + static class SpringDocTestApp {} + + @JsonTypeInfo(use = Id.NAME, property = "@type") + @JsonSubTypes(@Type(CommonImplementor.class)) + interface FirstHierarchy {} + + @JsonTypeInfo(use = Id.NAME, property = "@type") + @JsonSubTypes(@Type(CommonImplementor.class)) + interface SecondHierarchy {} + + class CommonImplementor implements FirstHierarchy, SecondHierarchy {} + + record CommonImplementorUser(FirstHierarchy firstHierarchy, SecondHierarchy secondHierarchy) {} + + record FirstHierarchyUser(FirstHierarchy firstHierarchy) {} + + record SecondHierarchyUser(SecondHierarchy secondHierarchy) {} +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app222.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app222.json new file mode 100644 index 000000000..cd4de9975 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app222.json @@ -0,0 +1,122 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/hello2": { + "get": { + "tags": [ + "hello-controller" + ], + "operationId": "getItems2", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SecondHierarchyUser" + } + } + } + } + } + } + }, + "/hello1": { + "get": { + "tags": [ + "hello-controller" + ], + "operationId": "getItems1", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/FirstHierarchyUser" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "CommonImplementor": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/SecondHierarchy" + }, + { + "$ref": "#/components/schemas/FirstHierarchy" + } + ] + }, + "SecondHierarchy": { + "required": [ + "@type" + ], + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "discriminator": { + "propertyName": "@type" + } + }, + "SecondHierarchyUser": { + "type": "object", + "properties": { + "secondHierarchy": { + "oneOf": [ + { + "$ref": "#/components/schemas/CommonImplementor" + } + ] + } + } + }, + "FirstHierarchy": { + "required": [ + "@type" + ], + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "discriminator": { + "propertyName": "@type" + } + }, + "FirstHierarchyUser": { + "type": "object", + "properties": { + "firstHierarchy": { + "oneOf": [ + { + "$ref": "#/components/schemas/CommonImplementor" + } + ] + } + } + } + } + } +}