Repository: springdoc/springdoc-openapi
Pull request #2345 (2023-08-15 23:03:53)

Support Schema added in OpenAPI Specification v3.1
fix: #2342



Patch:
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 4cf44cf6e..7660ebe2f 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
@@ -74,6 +74,7 @@ import org.springdoc.core.filters.OpenApiMethodFilter;
 import org.springdoc.core.models.GroupedOpenApi;
 import org.springdoc.core.parsers.ReturnTypeParser;
 import org.springdoc.core.properties.SpringDocConfigProperties;
+import org.springdoc.core.properties.SpringDocConfigProperties.ApiDocs.OpenApiVersion;
 import org.springdoc.core.providers.ActuatorProvider;
 import org.springdoc.core.providers.CloudFunctionProvider;
 import org.springdoc.core.providers.JavadocProvider;
@@ -297,8 +298,9 @@ public class SpringDocConfiguration {
 	 */
 	@Bean
 	@Lazy(false)
-	ModelConverterRegistrar modelConverterRegistrar(Optional<List<ModelConverter>> modelConverters) {
-		return new ModelConverterRegistrar(modelConverters.orElse(Collections.emptyList()));
+	ModelConverterRegistrar modelConverterRegistrar(Optional<List<ModelConverter>> modelConverters, SpringDocConfigProperties springDocConfigProperties) {
+		boolean openapi31 = OpenApiVersion.OPENAPI_3_1 == springDocConfigProperties.getApiDocs().getVersion();
+		return new ModelConverterRegistrar(modelConverters.orElse(Collections.emptyList()), openapi31);
 	}
 
 	/**
@@ -637,10 +639,11 @@ public class SpringDocConfiguration {
 		@Bean
 		@ConditionalOnMissingBean
 		@Lazy(false)
-		QuerydslPredicateOperationCustomizer queryDslQuerydslPredicateOperationCustomizer(Optional<QuerydslBindingsFactory> querydslBindingsFactory) {
+		QuerydslPredicateOperationCustomizer queryDslQuerydslPredicateOperationCustomizer(Optional<QuerydslBindingsFactory> querydslBindingsFactory, SpringDocConfigProperties springDocConfigProperties) {
 			if (querydslBindingsFactory.isPresent()) {
 				getConfig().addRequestWrapperToIgnore(Predicate.class);
-				return new QuerydslPredicateOperationCustomizer(querydslBindingsFactory.get());
+				boolean openapi31 = OpenApiVersion.OPENAPI_3_1 == springDocConfigProperties.getApiDocs().getVersion();
+				return new QuerydslPredicateOperationCustomizer(querydslBindingsFactory.get(), openapi31);
 			}
 			return null;
 		}
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocDataRestConfiguration.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocDataRestConfiguration.java
index f36393e54..b176de49f 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocDataRestConfiguration.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocDataRestConfiguration.java
@@ -36,6 +36,7 @@ import org.springdoc.core.data.DataRestRouterOperationService;
 import org.springdoc.core.data.DataRestTagsService;
 import org.springdoc.core.discoverer.SpringDocParameterNameDiscoverer;
 import org.springdoc.core.properties.SpringDocConfigProperties;
+import org.springdoc.core.properties.SpringDocConfigProperties.ApiDocs.OpenApiVersion;
 import org.springdoc.core.providers.DataRestHalProvider;
 import org.springdoc.core.providers.ObjectMapperProvider;
 import org.springdoc.core.providers.SpringRepositoryRestResourceProvider;
@@ -232,8 +233,9 @@ public class SpringDocDataRestConfiguration {
 		@Bean
 		@ConditionalOnMissingBean
 		@Lazy(false)
-		SpringDocDataRestUtils springDocDataRestUtils(LinkRelationProvider linkRelationProvider, RepositoryRestConfiguration repositoryRestConfiguration) {
-			return new SpringDocDataRestUtils(linkRelationProvider, repositoryRestConfiguration);
+		SpringDocDataRestUtils springDocDataRestUtils(LinkRelationProvider linkRelationProvider, RepositoryRestConfiguration repositoryRestConfiguration, SpringDocConfigProperties springDocConfigProperties) {
+			boolean openapi31 = OpenApiVersion.OPENAPI_3_1 == springDocConfigProperties.getApiDocs().getVersion();
+			return new SpringDocDataRestUtils(linkRelationProvider, repositoryRestConfiguration, openapi31);
 		}
 	}
 
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/ModelConverterRegistrar.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/ModelConverterRegistrar.java
index a92701b97..5681b72e8 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/ModelConverterRegistrar.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/ModelConverterRegistrar.java
@@ -33,6 +33,8 @@ import io.swagger.v3.core.converter.ModelConverters;
 import org.apache.commons.lang3.reflect.FieldUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springdoc.core.properties.SpringDocConfigProperties;
+import org.springdoc.core.utils.SpringDocAnnotationsUtils;
 
 /**
  * Wrapper for model converters to only register converters once
@@ -43,7 +45,7 @@ public class ModelConverterRegistrar {
 	/**
 	 * The constant modelConvertersInstance.
 	 */
-	private static final ModelConverters modelConvertersInstance = ModelConverters.getInstance();
+	private static ModelConverters modelConvertersInstance = null;
 
 	/**
 	 * The constant LOGGER.
@@ -55,7 +57,9 @@ public class ModelConverterRegistrar {
 	 *
 	 * @param modelConverters spring registered model converter beans which have to be                        registered in {@link ModelConverters} instance
 	 */
-	public ModelConverterRegistrar(List<ModelConverter> modelConverters) {
+	public ModelConverterRegistrar(List<ModelConverter> modelConverters, boolean openapi31) {
+		modelConvertersInstance = ModelConverters.getInstance(openapi31);
+		SpringDocAnnotationsUtils.setOpenapi31(openapi31);
 		for (ModelConverter modelConverter : modelConverters) {
 			Optional<ModelConverter> registeredConverterOptional = getRegisteredConverterSameAs(modelConverter);
 			registeredConverterOptional.ifPresent(modelConvertersInstance::removeConverter);
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/OpenApiHateoasLinksCustomizer.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/OpenApiHateoasLinksCustomizer.java
index bf0f50701..661cd82e4 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/OpenApiHateoasLinksCustomizer.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/OpenApiHateoasLinksCustomizer.java
@@ -34,6 +34,7 @@ import io.swagger.v3.oas.models.media.MapSchema;
 import io.swagger.v3.oas.models.media.ObjectSchema;
 import io.swagger.v3.oas.models.media.StringSchema;
 import org.springdoc.core.properties.SpringDocConfigProperties;
+import org.springdoc.core.properties.SpringDocConfigProperties.ApiDocs.OpenApiVersion;
 
 import org.springframework.hateoas.Link;
 
@@ -59,7 +60,8 @@ public class OpenApiHateoasLinksCustomizer extends SpecFilter implements GlobalO
 
 	@Override
 	public void customise(OpenAPI openApi) {
-		ResolvedSchema resolvedLinkSchema = ModelConverters.getInstance()
+		boolean openapi31 = OpenApiVersion.OPENAPI_3_1 == springDocConfigProperties.getApiDocs().getVersion();
+		ResolvedSchema resolvedLinkSchema = ModelConverters.getInstance(openapi31)
 				.resolveAsResolvedSchema(new AnnotatedType(Link.class));
 		openApi
 				.schema("Link", resolvedLinkSchema.schema)
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/QuerydslPredicateOperationCustomizer.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/QuerydslPredicateOperationCustomizer.java
index 5ce719080..3cf5cacc6 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/QuerydslPredicateOperationCustomizer.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/QuerydslPredicateOperationCustomizer.java
@@ -76,13 +76,19 @@ public class QuerydslPredicateOperationCustomizer implements GlobalOperationCust
 	 */
 	private final QuerydslBindingsFactory querydslBindingsFactory;
 
+	/**
+	 * OpenAPI v3.1
+	 */
+	private final boolean openapi31;
+
 	/**
 	 * Instantiates a new Querydsl predicate operation customizer.
 	 *
 	 * @param querydslBindingsFactory the querydsl bindings factory
 	 */
-	public QuerydslPredicateOperationCustomizer(QuerydslBindingsFactory querydslBindingsFactory) {
+	public QuerydslPredicateOperationCustomizer(QuerydslBindingsFactory querydslBindingsFactory, boolean openapi31) {
 		this.querydslBindingsFactory = querydslBindingsFactory;
+		this.openapi31 = openapi31;
 	}
 
 	@Override
@@ -286,7 +292,7 @@ public class QuerydslPredicateOperationCustomizer implements GlobalOperationCust
 				schema = primitiveType.createProperty();
 			}
 			else {
-				ResolvedSchema resolvedSchema = ModelConverters.getInstance()
+				ResolvedSchema resolvedSchema = ModelConverters.getInstance(this.openapi31)
 						.resolveAsResolvedSchema(
 								new io.swagger.v3.core.converter.AnnotatedType(type).resolveAsRef(true));
 				// could not resolve the schema or this schema references other schema
@@ -303,4 +309,4 @@ public class QuerydslPredicateOperationCustomizer implements GlobalOperationCust
 		}
 		return parameter;
 	}
-}
\ No newline at end of file
+}
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 b50abd3ff..f6c39c2be 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
@@ -85,6 +85,8 @@ public class SpringDocAnnotationsUtils extends AnnotationsUtils {
 		ANNOTATIONS_TO_IGNORE.add(RequestAttribute.class);
 	}
 
+	private static boolean OPENAPI31 = false;
+
 	/**
 	 * Resolve schema from type schema.
 	 *
@@ -118,7 +120,7 @@ public class SpringDocAnnotationsUtils extends AnnotationsUtils {
 		Schema schemaN = null;
 		ResolvedSchema resolvedSchema;
 		try {
-			resolvedSchema = ModelConverters.getInstance()
+			resolvedSchema = ModelConverters.getInstance(OPENAPI31)
 					.resolveAsResolvedSchema(
 							new AnnotatedType(returnType).resolveAsRef(true).jsonViewAnnotation(jsonView).ctxAnnotations(annotations));
 		}
@@ -283,6 +285,15 @@ public class SpringDocAnnotationsUtils extends AnnotationsUtils {
 			ANNOTATIONS_TO_IGNORE.removeAll(Arrays.asList(classes));
 	}
 
+	/**
+	 * set openapi31
+	 *
+	 * @param openapi31
+	 */
+	public static void setOpenapi31(boolean openapi31) {
+		OPENAPI31 = openapi31;
+	}
+
 	/**
 	 * Add encoding to media type.
 	 *
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocDataRestUtils.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocDataRestUtils.java
index c091d65e3..9f457639b 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocDataRestUtils.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocDataRestUtils.java
@@ -87,6 +87,12 @@ public class SpringDocDataRestUtils {
 	 */
 	private final LinkRelationProvider linkRelationProvider;
 
+	/**
+	 * OpenAPI v3.1
+	 */
+	private final boolean openapi31;
+
+
 	/**
 	 * The Entity ino map.
 	 */
@@ -103,9 +109,10 @@ public class SpringDocDataRestUtils {
 	 * @param linkRelationProvider the link relation provider
 	 * @param repositoryRestConfiguration the repository rest configuration
 	 */
-	public SpringDocDataRestUtils(LinkRelationProvider linkRelationProvider, RepositoryRestConfiguration repositoryRestConfiguration) {
+	public SpringDocDataRestUtils(LinkRelationProvider linkRelationProvider, RepositoryRestConfiguration repositoryRestConfiguration, boolean openapi31) {
 		this.linkRelationProvider = linkRelationProvider;
 		this.repositoryRestConfiguration = repositoryRestConfiguration;
+		this.openapi31 = openapi31;
 	}
 
 	/**
@@ -205,7 +212,7 @@ public class SpringDocDataRestUtils {
 		schema.set$ref(newKey);
 		//create new schema
 		Class schemaImplementation = entityInoMap.get(className).getDomainType();
-		ResolvedSchema resolvedSchema = ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(schemaImplementation));
+		ResolvedSchema resolvedSchema = ModelConverters.getInstance(openapi31).readAllAsResolvedSchema(new AnnotatedType().type(schemaImplementation));
 		Map<String, Schema> schemaMap;
 		if (resolvedSchema != null) {
 			schemaMap = resolvedSchema.referencedSchemas;
@@ -302,7 +309,7 @@ public class SpringDocDataRestUtils {
 	private Schema createNewResponseSchema(String className, Components components) {
 		Class schemaImplementation = entityInoMap.get(className).getDomainType();
 		Schema schemaObject = new Schema();
-		ResolvedSchema resolvedSchema = ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(schemaImplementation));
+		ResolvedSchema resolvedSchema = ModelConverters.getInstance(openapi31).readAllAsResolvedSchema(new AnnotatedType().type(schemaImplementation));
 		Map<String, Schema> schemaMap;
 		if (resolvedSchema != null) {
 			schemaMap = resolvedSchema.referencedSchemas;


Test patch:
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app7/ExamplesController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app7/ExamplesController.java
new file mode 100644
index 000000000..5c212d626
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app7/ExamplesController.java
@@ -0,0 +1,36 @@
+/*
+ *
+ *  *
+ *  *  *
+ *  *  *  * Copyright 2019-2023 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.app7;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class ExamplesController {
+
+	@GetMapping(value = "/")
+	public ExamplesResponse index() {
+		return null;
+	}
+}
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app7/ExamplesResponse.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app7/ExamplesResponse.java
new file mode 100644
index 000000000..8ad235bc0
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app7/ExamplesResponse.java
@@ -0,0 +1,12 @@
+package test.org.springdoc.api.v31.app7;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
+
+public record ExamplesResponse(
+		@Schema(description = "name", requiredMode = REQUIRED, examples = { "name" })
+		String name,
+		@Schema(description = "subject", requiredMode = REQUIRED, example = "Hello", examples = { "Hello", "World" })
+		String subject
+) {}
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app7/SpringDocApp7Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app7/SpringDocApp7Test.java
new file mode 100644
index 000000000..22d05ba05
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app7/SpringDocApp7Test.java
@@ -0,0 +1,35 @@
+/*
+ *
+ *  *
+ *  *  *
+ *  *  *  * Copyright 2019-2023 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.app7;
+
+import test.org.springdoc.api.v31.AbstractSpringDocV31Test;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+public class SpringDocApp7Test extends AbstractSpringDocV31Test {
+
+	@SpringBootApplication
+	static class SpringDocTestApp {
+
+	}
+}
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app7.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app7.json
new file mode 100644
index 000000000..60bf98686
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app7.json
@@ -0,0 +1,63 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "OpenAPI definition",
+    "version": "v0"
+  },
+  "servers": [
+    {
+      "url": "http://localhost",
+      "description": "Generated server url"
+    }
+  ],
+  "paths": {
+    "/": {
+      "get": {
+        "tags": [
+          "examples-controller"
+        ],
+        "operationId": "index",
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "*/*": {
+                "schema": {
+                  "$ref": "#/components/schemas/ExamplesResponse"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "ExamplesResponse": {
+        "properties": {
+          "name": {
+            "type": "string",
+            "description": "name",
+            "examples": [
+              "name"
+            ]
+          },
+          "subject": {
+            "type": "string",
+            "description": "subject",
+            "example":"Hello",
+            "examples": [
+              "Hello",
+              "World"
+            ]
+          }
+        },
+        "required": [
+          "name",
+          "subject"
+        ]
+      }
+    }
+  }
+}