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 8cdbd65a6..9a940d349 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 @@ -37,7 +37,9 @@ import java.util.Set; import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.BeanDescription; import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import io.swagger.v3.core.converter.AnnotatedType; import io.swagger.v3.core.converter.ModelConverter; import io.swagger.v3.core.converter.ModelConverterContext; @@ -120,17 +122,28 @@ else if (resolvedSchema.getProperties().containsKey(javaType.getRawClass().getSi 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)) { + BeanDescription javaTypeIntrospection = springDocObjectMapper.jsonMapper().getDeserializationConfig().introspect(javaType); + for (BeanPropertyDefinition property : javaTypeIntrospection.findProperties()) { + boolean isUnwrapped = (property.getField() != null && property.getField().hasAnnotation(JsonUnwrapped.class)) || + (property.getGetter() != null && property.getGetter().hasAnnotation(JsonUnwrapped.class)); + + if (isUnwrapped) { if (!TypeNameResolver.std.getUseFqn()) PARENT_TYPES_TO_IGNORE.add(javaType.getRawClass().getSimpleName()); else PARENT_TYPES_TO_IGNORE.add(javaType.getRawClass().getName()); } - else if (field.isAnnotationPresent(io.swagger.v3.oas.annotations.media.Schema.class)) { - io.swagger.v3.oas.annotations.media.Schema declaredSchema = field.getDeclaredAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); - if (ArrayUtils.isNotEmpty(declaredSchema.oneOf()) || ArrayUtils.isNotEmpty(declaredSchema.allOf())) { - TYPES_TO_SKIP.add(field.getType().getSimpleName()); + else { + io.swagger.v3.oas.annotations.media.Schema declaredSchema = null; + if (property.getField() != null) { + declaredSchema = property.getField().getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + } else if (property.getGetter() != null) { + declaredSchema = property.getGetter().getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + } + + if (declaredSchema != null && + (ArrayUtils.isNotEmpty(declaredSchema.oneOf()) || ArrayUtils.isNotEmpty(declaredSchema.allOf()))) { + TYPES_TO_SKIP.add(property.getPrimaryType().getRawClass().getSimpleName()); } } } diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app239/RootModel.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app239/RootModel.java new file mode 100644 index 000000000..cf2bd6039 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app239/RootModel.java @@ -0,0 +1,27 @@ +package test.org.springdoc.api.v30.app239; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; + +public class RootModel { + + private Integer rootProperty; + + private UnwrappedModel unwrappedModel; + + public Integer getRootProperty() { + return rootProperty; + } + + public void setRootProperty(Integer rootProperty) { + this.rootProperty = rootProperty; + } + + @JsonUnwrapped + public UnwrappedModel getUnwrappedModel() { + return unwrappedModel; + } + + public void setUnwrappedModel(UnwrappedModel unwrappedModel) { + this.unwrappedModel = unwrappedModel; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app239/SpringDocApp239Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app239/SpringDocApp239Test.java new file mode 100644 index 000000000..122f279a5 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app239/SpringDocApp239Test.java @@ -0,0 +1,34 @@ +/* + * + * * + * * * + * * * * + * * * * * Copyright 2019-2024 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.app239; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import test.org.springdoc.api.v30.AbstractSpringDocV30Test; + +public class SpringDocApp239Test extends AbstractSpringDocV30Test { + + @SpringBootApplication + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app239/TestController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app239/TestController.java new file mode 100644 index 000000000..a3f084691 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app239/TestController.java @@ -0,0 +1,15 @@ +package test.org.springdoc.api.v30.app239; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class TestController { + + @GetMapping + public RootModel getRootModel() { + return new RootModel(); + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app239/UnwrappedModel.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app239/UnwrappedModel.java new file mode 100644 index 000000000..9ac5c2a48 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app239/UnwrappedModel.java @@ -0,0 +1,14 @@ +package test.org.springdoc.api.v30.app239; + +public class UnwrappedModel { + + private Integer unwrappedProperty; + + public Integer getUnwrappedProperty() { + return unwrappedProperty; + } + + public void setUnwrappedProperty(Integer unwrappedProperty) { + this.unwrappedProperty = unwrappedProperty; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app224/RootModel.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app224/RootModel.java index 0df928bc6..8e4c9ce6a 100644 --- a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app224/RootModel.java +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app224/RootModel.java @@ -6,7 +6,6 @@ public class RootModel { private Integer rootProperty; - @JsonUnwrapped private UnwrappedModel unwrappedModel; public Integer getRootProperty() { @@ -17,6 +16,7 @@ public void setRootProperty(Integer rootProperty) { this.rootProperty = rootProperty; } + @JsonUnwrapped public UnwrappedModel getUnwrappedModel() { return unwrappedModel; } diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app239/RootModel.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app239/RootModel.java new file mode 100644 index 000000000..ea8b4261f --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app239/RootModel.java @@ -0,0 +1,27 @@ +package test.org.springdoc.api.v31.app239; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; + +public class RootModel { + + private Integer rootProperty; + + @JsonUnwrapped + private UnwrappedModel unwrappedModel; + + public Integer getRootProperty() { + return rootProperty; + } + + public void setRootProperty(Integer rootProperty) { + this.rootProperty = rootProperty; + } + + public UnwrappedModel getUnwrappedModel() { + return unwrappedModel; + } + + public void setUnwrappedModel(UnwrappedModel unwrappedModel) { + this.unwrappedModel = unwrappedModel; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app239/SpringDocApp239Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app239/SpringDocApp239Test.java new file mode 100644 index 000000000..efa593ebc --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app239/SpringDocApp239Test.java @@ -0,0 +1,34 @@ +/* + * + * * + * * * + * * * * + * * * * * Copyright 2019-2024 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.v31.app239; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import test.org.springdoc.api.v31.AbstractSpringDocTest; + +public class SpringDocApp239Test extends AbstractSpringDocTest { + + @SpringBootApplication + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app239/TestController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app239/TestController.java new file mode 100644 index 000000000..623fbaefa --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app239/TestController.java @@ -0,0 +1,15 @@ +package test.org.springdoc.api.v31.app239; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class TestController { + + @GetMapping + public RootModel getRootModel() { + return new RootModel(); + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app239/UnwrappedModel.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app239/UnwrappedModel.java new file mode 100644 index 000000000..ab4776f64 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app239/UnwrappedModel.java @@ -0,0 +1,14 @@ +package test.org.springdoc.api.v31.app239; + +public class UnwrappedModel { + + private Integer unwrappedProperty; + + public Integer getUnwrappedProperty() { + return unwrappedProperty; + } + + public void setUnwrappedProperty(Integer unwrappedProperty) { + this.unwrappedProperty = unwrappedProperty; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app239.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app239.json new file mode 100644 index 000000000..d83020e50 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app239.json @@ -0,0 +1,52 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/api": { + "get": { + "tags": [ + "test-controller" + ], + "operationId": "getRootModel", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RootModel" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "RootModel": { + "type": "object", + "properties": { + "rootProperty": { + "type": "integer", + "format": "int32" + }, + "unwrappedProperty": { + "type": "integer", + "format": "int32" + } + } + } + } + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app239.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app239.json new file mode 100644 index 000000000..0ef50ddff --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app239.json @@ -0,0 +1,52 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/api": { + "get": { + "tags": [ + "test-controller" + ], + "operationId": "getRootModel", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RootModel" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "RootModel": { + "type": "object", + "properties": { + "rootProperty": { + "type": "integer", + "format": "int32" + }, + "unwrappedProperty": { + "type": "integer", + "format": "int32" + } + } + } + } + } +}