From ddf718ebdc21f447d1232acd8ba9d03e0c79b3f4 Mon Sep 17 00:00:00 2001 From: Martin Rueckl Date: Thu, 2 Jan 2025 11:17:09 +0100 Subject: [PATCH 1/5] add testcase to reproduce error --- .../springdoc/api/app13/SpringDocApp13Test.kt | 29 ++++++++ .../org/springdoc/api/app13/TestController.kt | 49 +++++++++++++ .../src/test/resources/results/app13.json | 71 +++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/SpringDocApp13Test.kt create mode 100644 springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/TestController.kt create mode 100644 springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/SpringDocApp13Test.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/SpringDocApp13Test.kt new file mode 100644 index 000000000..61da07022 --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/SpringDocApp13Test.kt @@ -0,0 +1,29 @@ +/* + * + * * 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 test.org.springdoc.api.app13 + +import org.springframework.boot.autoconfigure.SpringBootApplication +import test.org.springdoc.api.AbstractKotlinSpringDocMVCTest + +class SpringDocApp13Test : AbstractKotlinSpringDocMVCTest() { + + @SpringBootApplication + class DemoApplication + +} diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/TestController.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/TestController.kt new file mode 100644 index 000000000..7fe6d28cb --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/TestController.kt @@ -0,0 +1,49 @@ +/* + * + * * 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 test.org.springdoc.api.app13 + + +import io.swagger.v3.oas.annotations.media.Schema +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 + +@Schema(description = "Generic description") +data class KeyValue( + val key: String, + val value: String, +) + +@Schema +data class SomeDTO( + @Schema(description = "Description A", allOf = [KeyValue::class]) val fieldA: KeyValue, + @Schema(description = "Description B", allOf = [KeyValue::class]) val fieldB: KeyValue, +) + + +@RestController +@RequestMapping("/test") +class TestController { + @PostMapping("/test") + fun create(@RequestBody some: SomeDTO) { + + } +} + diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json new file mode 100644 index 000000000..840177b22 --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json @@ -0,0 +1,71 @@ +{ + "openapi" : "3.0.1", + "info" : { + "title" : "OpenAPI definition", + "version" : "v0" + }, + "servers" : [ { + "url" : "http://localhost", + "description" : "Generated server url" + } ], + "paths" : { + "/test/test" : { + "post" : { + "tags" : [ "test-controller" ], + "operationId" : "create", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SomeDTO" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "description" : "OK" + } + } + } + } + }, + "components" : { + "schemas" : { + "KeyValue" : { + "required" : [ "key", "value" ], + "type" : "object", + "properties" : { + "key" : { + "type" : "string" + }, + "value" : { + "type" : "string" + } + }, + "description" : "Generic description" + }, + "SomeDTO" : { + "required" : [ "fieldA", "fieldB" ], + "type" : "object", + "properties" : { + "fieldA" : { + "type" : "object", + "description" : "Description A", + "allOf" : [ { + "$ref" : "#/components/schemas/KeyValue" + } ] + }, + "fieldB" : { + "type" : "object", + "description" : "Description B", + "allOf" : [ { + "$ref" : "#/components/schemas/KeyValue" + } ] + } + } + } + } + } +} \ No newline at end of file From 77ff9002c93984ebd5d6333a82eaf0d3a042f01e Mon Sep 17 00:00:00 2001 From: Martin Rueckl Date: Thu, 2 Jan 2025 14:09:25 +0100 Subject: [PATCH 2/5] small refactor of schema resolution for polymorphic models --- .../converters/PolymorphicModelConverter.java | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) 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 12cec2add..9eb237e20 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 @@ -21,7 +21,7 @@ * * * * * * * * * - * + * */ package org.springdoc.core.converters; @@ -110,32 +110,35 @@ else if (resolvedSchema.getProperties().containsKey(javaType.getRawClass().getSi @Override public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterator chain) { JavaType javaType = springDocObjectMapper.jsonMapper().constructType(type.getType()); - if (javaType != null) { - for (Field field : FieldUtils.getAllFields(javaType.getRawClass())) { - if (field.isAnnotationPresent(JsonUnwrapped.class)) { - PARENT_TYPES_TO_IGNORE.add(javaType.getRawClass().getSimpleName()); - } + + if (javaType == null || !chain.hasNext()) { + return null; + } + + for (Field field : FieldUtils.getAllFields(javaType.getRawClass())) { + if (field.isAnnotationPresent(JsonUnwrapped.class)) { + PARENT_TYPES_TO_IGNORE.add(javaType.getRawClass().getSimpleName()); } - if (chain.hasNext()) { - if (!type.isResolveAsRef() && type.getParent() != null - && PARENT_TYPES_TO_IGNORE.stream().noneMatch(ignore -> type.getParent().getName().startsWith(ignore))) - type.resolveAsRef(true); - Schema resolvedSchema = chain.next().resolve(type, context, chain); - resolvedSchema = getResolvedSchema(javaType, resolvedSchema); - if (resolvedSchema == null || resolvedSchema.get$ref() == null) { - return resolvedSchema; - } - if(resolvedSchema.get$ref().contains(Components.COMPONENTS_SCHEMAS_REF)) { - String schemaName = resolvedSchema.get$ref().substring(Components.COMPONENTS_SCHEMAS_REF.length()); - Schema existingSchema = context.getDefinedModels().get(schemaName); - if (existingSchema != null && existingSchema.getOneOf() != null) { - return resolvedSchema; - } - } - return composePolymorphicSchema(type, resolvedSchema, context.getDefinedModels().values()); + } + + String parentName = type.getParent() == null ? null : type.getParent().getName(); + if (parentName != null && PARENT_TYPES_TO_IGNORE.stream().noneMatch(parentName::startsWith)){ + type.resolveAsRef(true); + } + + Schema resolvedSchema = getResolvedSchema(javaType, chain.next().resolve(type, context, chain)); + if (resolvedSchema == null || resolvedSchema.get$ref() == null) { + return resolvedSchema; + } + + if(resolvedSchema.get$ref().contains(Components.COMPONENTS_SCHEMAS_REF)) { + String schemaName = resolvedSchema.get$ref().substring(Components.COMPONENTS_SCHEMAS_REF.length()); + Schema existingSchema = context.getDefinedModels().get(schemaName); + if (existingSchema != null && existingSchema.getOneOf() != null) { + return resolvedSchema; } } - return null; + return composePolymorphicSchema(type, resolvedSchema, context.getDefinedModels().values()); } /** From fbd8bcd1b53e3e04bf8cadb4e8c530a279b6b5f2 Mon Sep 17 00:00:00 2001 From: Martin Rueckl Date: Thu, 2 Jan 2025 14:42:43 +0100 Subject: [PATCH 3/5] resolve infinite recursion --- .../converters/PolymorphicModelConverter.java | 2 +- .../src/test/resources/results/app13.json | 33 +++++++++---------- 2 files changed, 16 insertions(+), 19 deletions(-) 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 9eb237e20..1673ce8a1 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 @@ -134,7 +134,7 @@ public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterato if(resolvedSchema.get$ref().contains(Components.COMPONENTS_SCHEMAS_REF)) { String schemaName = resolvedSchema.get$ref().substring(Components.COMPONENTS_SCHEMAS_REF.length()); Schema existingSchema = context.getDefinedModels().get(schemaName); - if (existingSchema != null && existingSchema.getOneOf() != null) { + if (existingSchema != null && (existingSchema.getOneOf() != null || existingSchema.getAllOf() != null || existingSchema.getAnyOf() != null)) { return resolvedSchema; } } diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json index 840177b22..473e9bb2f 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json @@ -36,33 +36,30 @@ "KeyValue" : { "required" : [ "key", "value" ], "type" : "object", - "properties" : { - "key" : { - "type" : "string" - }, - "value" : { - "type" : "string" + "description" : "Generic description", + "allOf" : [ { + "$ref" : "#/components/schemas/KeyValue" + }, { + "type" : "object", + "properties" : { + "key" : { + "type" : "string" + }, + "value" : { + "type" : "string" + } } - }, - "description" : "Generic description" + } ] }, "SomeDTO" : { "required" : [ "fieldA", "fieldB" ], "type" : "object", "properties" : { "fieldA" : { - "type" : "object", - "description" : "Description A", - "allOf" : [ { - "$ref" : "#/components/schemas/KeyValue" - } ] + "$ref" : "#/components/schemas/KeyValue" }, "fieldB" : { - "type" : "object", - "description" : "Description B", - "allOf" : [ { - "$ref" : "#/components/schemas/KeyValue" - } ] + "$ref" : "#/components/schemas/KeyValue" } } } From a9148f4ad244edc5b300c35e21300c703639c50f Mon Sep 17 00:00:00 2001 From: Martin Rueckl Date: Thu, 2 Jan 2025 16:27:19 +0100 Subject: [PATCH 4/5] changed to OpenAPI v3.1 --- .../springdoc/api/app13/SpringDocApp13Test.kt | 20 +++++++++++++ .../org/springdoc/api/app13/TestController.kt | 2 ++ .../src/test/resources/results/app13.json | 29 ++++++++++++++----- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/SpringDocApp13Test.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/SpringDocApp13Test.kt index 61da07022..1f2f41145 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/SpringDocApp13Test.kt +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/SpringDocApp13Test.kt @@ -18,11 +18,31 @@ package test.org.springdoc.api.app13 +import org.springdoc.core.properties.SpringDocConfigProperties +import org.springdoc.core.properties.SpringDocConfigProperties.ApiDocs.OpenApiVersion import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration import test.org.springdoc.api.AbstractKotlinSpringDocMVCTest + +@SpringBootTest//(classes = [Config::class]) class SpringDocApp13Test : AbstractKotlinSpringDocMVCTest() { + + @Configuration + class Config{ + @Bean + fun springDocConfigProperties():SpringDocConfigProperties{ + val x= SpringDocConfigProperties() + x.apiDocs.version = OpenApiVersion.OPENAPI_3_1 + return x + } + + } + + @SpringBootApplication class DemoApplication diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/TestController.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/TestController.kt index 7fe6d28cb..8c4805e53 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/TestController.kt +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/TestController.kt @@ -19,6 +19,7 @@ package test.org.springdoc.api.app13 +import io.swagger.v3.oas.annotations.OpenAPI31 import io.swagger.v3.oas.annotations.media.Schema import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody @@ -38,6 +39,7 @@ data class SomeDTO( ) + @RestController @RequestMapping("/test") class TestController { diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json index 473e9bb2f..932813b1e 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json @@ -52,14 +52,29 @@ } ] }, "SomeDTO" : { - "required" : [ "fieldA", "fieldB" ], - "type" : "object", - "properties" : { - "fieldA" : { - "$ref" : "#/components/schemas/KeyValue" + "required": [ + "fieldA", + "fieldB" + ], + "type": "object", + "properties": { + "fieldA": { + "type": "object", + "description": "Description A", + "allOf": [ + { + "$ref": "#/components/schemas/KeyValue" + } + ] }, - "fieldB" : { - "$ref" : "#/components/schemas/KeyValue" + "fieldB": { + "type": "object", + "description": "Description B", + "allOf": [ + { + "$ref": "#/components/schemas/KeyValue" + } + ] } } } From c4d207810919d904d857c65aa30eabba3aa6fa8f Mon Sep 17 00:00:00 2001 From: Martin Rueckl Date: Thu, 2 Jan 2025 16:30:56 +0100 Subject: [PATCH 5/5] working test case --- .../org/springdoc/api/app13/TestController.kt | 4 +-- .../src/test/resources/results/app13.json | 35 ++++++------------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/TestController.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/TestController.kt index 8c4805e53..7ec21bac8 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/TestController.kt +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app13/TestController.kt @@ -34,8 +34,8 @@ data class KeyValue( @Schema data class SomeDTO( - @Schema(description = "Description A", allOf = [KeyValue::class]) val fieldA: KeyValue, - @Schema(description = "Description B", allOf = [KeyValue::class]) val fieldB: KeyValue, + @Schema(description = "Description A") val fieldA: KeyValue, + @Schema(description = "Description B") val fieldB: KeyValue, ) diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json index 932813b1e..6bf07eb5b 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app13.json @@ -1,5 +1,5 @@ { - "openapi" : "3.0.1", + "openapi" : "3.1.0", "info" : { "title" : "OpenAPI definition", "version" : "v0" @@ -37,19 +37,14 @@ "required" : [ "key", "value" ], "type" : "object", "description" : "Generic description", - "allOf" : [ { - "$ref" : "#/components/schemas/KeyValue" - }, { - "type" : "object", - "properties" : { - "key" : { - "type" : "string" - }, - "value" : { - "type" : "string" - } + "properties" : { + "key" : { + "type" : "string" + }, + "value" : { + "type" : "string" } - } ] + } }, "SomeDTO" : { "required": [ @@ -59,22 +54,12 @@ "type": "object", "properties": { "fieldA": { - "type": "object", "description": "Description A", - "allOf": [ - { - "$ref": "#/components/schemas/KeyValue" - } - ] + "$ref": "#/components/schemas/KeyValue" }, "fieldB": { - "type": "object", "description": "Description B", - "allOf": [ - { - "$ref": "#/components/schemas/KeyValue" - } - ] + "$ref": "#/components/schemas/KeyValue" } } }