From d6979a9a4016c43a9882570b3202760f3b9a4039 Mon Sep 17 00:00:00 2001 From: Sahil Kumar <45749746+sahil-ramagiri@users.noreply.github.com> Date: Sun, 1 Sep 2024 11:13:55 -0400 Subject: [PATCH] Do not require JsonSubType annotation for sealed classes. --- .../configuration/SpringDocConfiguration.java | 4 +- .../SpringDocSealedClassModule.java | 64 +++++ .../api/v30/app224/AbstractParent.java | 66 +++++ .../api/v30/app224/ConcreteParent.java | 66 +++++ .../springdoc/api/v30/app224/Controller.java | 68 ++++++ .../api/v30/app224/SpringDocApp224Test.java | 36 +++ .../api/v30/app225/HelloController.java | 51 ++++ .../api/v30/app225/SpringDocApp225Test.java | 35 +++ .../test/resources/results/3.0.1/app224.json | 227 ++++++++++++++++++ .../test/resources/results/3.0.1/app225.json | 132 ++++++++++ 10 files changed, 747 insertions(+), 2 deletions(-) create mode 100644 springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocSealedClassModule.java create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/AbstractParent.java create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/ConcreteParent.java create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/Controller.java create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/SpringDocApp224Test.java create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app225/HelloController.java create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app225/SpringDocApp225Test.java create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app224.json create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app225.json diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java index 3a51f92e2..2024be5d7 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java @@ -3,7 +3,7 @@ * * * * * * * * * - * * * * * Copyright 2019-2022 the original author or authors. + * * * * * 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. @@ -429,7 +429,7 @@ SpringDocProviders springDocProviders(Optional actuatorProvide Optional repositoryRestResourceProvider, Optional routerFunctionProvider, Optional springWebProvider, Optional webConversionServiceProvider, ObjectMapperProvider objectMapperProvider) { - objectMapperProvider.jsonMapper().registerModule(new SpringDocRequiredModule()); + objectMapperProvider.jsonMapper().registerModules(new SpringDocRequiredModule(), new SpringDocSealedClassModule()); return new SpringDocProviders(actuatorProvider, springCloudFunctionProvider, springSecurityOAuth2Provider, repositoryRestResourceProvider, routerFunctionProvider, springWebProvider, webConversionServiceProvider, objectMapperProvider); } diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocSealedClassModule.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocSealedClassModule.java new file mode 100644 index 000000000..d4821a151 --- /dev/null +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocSealedClassModule.java @@ -0,0 +1,64 @@ +/* + * + * * + * * * + * * * * Copyright 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 org.springdoc.core.configuration; + +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.databind.module.SimpleModule; +import io.swagger.v3.core.jackson.SwaggerAnnotationIntrospector; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * The type Spring doc sealed class module. + * + * @author sahil-ramagiri + */ +public class SpringDocSealedClassModule extends SimpleModule { + + @Override + public void setupModule(SetupContext context) { + context.insertAnnotationIntrospector(new RespectSealedClassAnnotationIntrospector()); + } + + /** + * The type sealed class annotation introspector. + */ + private static class RespectSealedClassAnnotationIntrospector extends SwaggerAnnotationIntrospector { + + @Override + public List findSubtypes(Annotated annotated) { + ArrayList subTypes = new ArrayList<>(); + + if (annotated.getAnnotated() instanceof Class clazz && clazz.isSealed()) { + Class permittedSubClasses = clazz.getPermittedSubclasses(); + if (permittedSubClasses.length > 0) { + Arrays.stream(permittedSubClasses).map(NamedType::new).forEach(subTypes::add); + } + } + + return subTypes; + } + } +} \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/AbstractParent.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/AbstractParent.java new file mode 100644 index 000000000..2562e132a --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/AbstractParent.java @@ -0,0 +1,66 @@ +/* + * + * * + * * * + * * * * + * * * * * 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.app224; + + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; + +@JsonTypeInfo(use = Id.NAME, property = "type") +public abstract sealed class AbstractParent { + private int id; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } +} + +class final ChildOfAbstract1 extends AbstractParent { + private String abstrachChild1Param; + + public String getAbstrachChild1Param() { + return abstrachChild1Param; + } + + public void setAbstrachChild1Param(String abstrachChild1Param) { + this.abstrachChild1Param = abstrachChild1Param; + } +} + +class final ChildOfAbstract2 extends AbstractParent { + private String abstractChild2Param; + + public String getAbstractChild2Param() { + return abstractChild2Param; + } + + public void setAbstractChild2Param(String abstractChild2Param) { + this.abstractChild2Param = abstractChild2Param; + } +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/ConcreteParent.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/ConcreteParent.java new file mode 100644 index 000000000..7a491adb4 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/ConcreteParent.java @@ -0,0 +1,66 @@ +/* + * + * * + * * * + * * * * + * * * * * 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.app224; + + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; + +@JsonTypeInfo(use = Id.NAME, property = "type") +public sealed class ConcreteParent { + private int id; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } +} + +final class ChildOfConcrete1 extends ConcreteParent { + private String concreteChild1Param; + + public String getConcreteChild1Param() { + return concreteChild1Param; + } + + public void setConcreteChild1Param(String concreteChild1Param) { + this.concreteChild1Param = concreteChild1Param; + } +} + +final 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-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/Controller.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/Controller.java new file mode 100644 index 000000000..a0ace3c02 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/Controller.java @@ -0,0 +1,68 @@ +/* + * + * * + * * * + * * * * + * * * * * 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.app224; + +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-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/SpringDocApp224Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/SpringDocApp224Test.java new file mode 100644 index 000000000..6e47dc7f1 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app224/SpringDocApp224Test.java @@ -0,0 +1,36 @@ +/* + * + * * + * * * + * * * * + * * * * * 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.app224; + +import test.org.springdoc.api.v30.AbstractSpringDocV30Test; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +public class SpringDocApp224Test extends AbstractSpringDocV30Test { + + @SpringBootApplication + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app225/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app225/HelloController.java new file mode 100644 index 000000000..94472d953 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app225/HelloController.java @@ -0,0 +1,51 @@ +package test.org.springdoc.api.v30.app225; + + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +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.RestController; + +@RestController +public class HelloController { + + @PostMapping("/parent") + public void parentEndpoint(@RequestBody Superclass parent) { + + } + +} + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type") +sealed class Superclass permits IntermediateClass { + + public Superclass() {} +} + +@Schema(name = IntermediateClass.SCHEMA_NAME) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type") +sealed class IntermediateClass extends Superclass permits FirstChildClass, SecondChildClass { + + public static final String SCHEMA_NAME = "IntermediateClass"; +} + +@Schema(name = FirstChildClass.SCHEMA_NAME) +final class FirstChildClass extends IntermediateClass { + + public static final String SCHEMA_NAME = "Image"; +} + +@Schema(name = SecondChildClass.SCHEMA_NAME) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type") +sealed class SecondChildClass extends IntermediateClass { + + public static final String SCHEMA_NAME = "Mail"; +} + +@Schema(name = ThirdChildClass.SCHEMA_NAME) +final class ThirdChildClass extends SecondChildClass { + + public static final String SCHEMA_NAME = "Home"; +} \ No newline at end of file diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app225/SpringDocApp225Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app225/SpringDocApp225Test.java new file mode 100644 index 000000000..d6e48bb1a --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app225/SpringDocApp225Test.java @@ -0,0 +1,35 @@ +/* + * + * * + * * * + * * * * + * * * * * 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.app225; + +import test.org.springdoc.api.v30.AbstractSpringDocV30Test; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +public class SpringDocApp225Test extends AbstractSpringDocV30Test { + + @SpringBootApplication + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app224.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app224.json new file mode 100644 index 000000000..1f5dfa891 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app224.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 diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app225.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app225.json new file mode 100644 index 000000000..de2244501 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app225.json @@ -0,0 +1,132 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/parent": { + "post": { + "tags": [ + "hello-controller" + ], + "operationId": "parentEndpoint", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Superclass" + }, + { + "$ref": "#/components/schemas/IntermediateClass" + }, + { + "$ref": "#/components/schemas/Image" + }, + { + "$ref": "#/components/schemas/Mail" + }, + { + "$ref": "#/components/schemas/Home" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "Home": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/Mail" + } + ] + }, + "Image": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/IntermediateClass" + } + ] + }, + "IntermediateClass": { + "required": [ + "@type" + ], + "type": "object", + "discriminator": { + "propertyName": "@type" + }, + "allOf": [ + { + "$ref": "#/components/schemas/Superclass" + }, + { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + } + } + ] + }, + "Mail": { + "required": [ + "@type" + ], + "type": "object", + "discriminator": { + "propertyName": "@type" + }, + "allOf": [ + { + "$ref": "#/components/schemas/IntermediateClass" + }, + { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + } + } + ] + }, + "Superclass": { + "required": [ + "@type" + ], + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "discriminator": { + "propertyName": "@type" + } + } + } + } +}