diff --git a/CHANGELOG.md b/CHANGELOG.md
index e71124f9c..0fde4af6c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [3.0.0] - 2025-11-21
+
+### Added
+- #2975 - Spring Framework 7 - Initial API versioning support
+- #3123 - Support static resources for webflux
+
+### Changed
+- Upgrade to Spring Boot 4.0.0
+- Upgrade to Scalar 0.4.3
+
+### Fixed
+
+- #3131 - Warning messages when docs are explicitly enabled
+- #3121 - NPE in KotlinDeprecatedPropertyCustomizer - resolvedSchema is null
+
## [3.0.0-RC1] - 2025-11-02
### Added
diff --git a/pom.xml b/pom.xml
index 9b614166d..46cbce477 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
org.springdoc
springdoc-openapi
- 3.0.0-RC1
+ 3.0.0
pom
Spring openapi documentation
Spring openapi documentation
@@ -11,7 +11,7 @@
org.springframework.boot
spring-boot-starter-parent
- 4.0.0-RC1
+ 4.0.0
@@ -35,7 +35,7 @@
scm:git:git@github.com:springdoc/springdoc-openapi.git
scm:git:git@github.com:springdoc/springdoc-openapi.git
- v3.0.0-RC1
+ v3.0.0
@@ -63,7 +63,7 @@
5.0.0-M1
2.0.0-M2
- 0.3.12
+ 0.4.3
false
@@ -123,11 +123,6 @@
spring-boot-starter-test
test
-
- org.springframework.boot
- spring-boot-jackson
- test
-
@@ -248,4 +243,15 @@
+
+
+
+ spring-snapshots
+ Spring Snapshots
+ https://repo.spring.io/snapshot
+
+ true
+
+
+
diff --git a/springdoc-openapi-bom/pom.xml b/springdoc-openapi-bom/pom.xml
index 0fcd42a83..61c3a1dfd 100644
--- a/springdoc-openapi-bom/pom.xml
+++ b/springdoc-openapi-bom/pom.xml
@@ -3,7 +3,7 @@
org.springdoc
springdoc-openapi
- 3.0.0-RC1
+ 3.0.0
springdoc-openapi-bom
${project.artifactId}
diff --git a/springdoc-openapi-starter-common/pom.xml b/springdoc-openapi-starter-common/pom.xml
index 57e7a24a7..eddb0ff99 100644
--- a/springdoc-openapi-starter-common/pom.xml
+++ b/springdoc-openapi-starter-common/pom.xml
@@ -3,7 +3,7 @@
org.springdoc
springdoc-openapi
- 3.0.0-RC1
+ 3.0.0
springdoc-openapi-starter-common
${project.artifactId}
@@ -20,6 +20,10 @@
io.swagger.core.v3
swagger-core-jakarta
+
+ org.springframework.boot
+ spring-boot-jackson
+
org.springframework.boot
spring-boot-configuration-processor
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
index c08a1a735..7ded98db0 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java
@@ -43,7 +43,9 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
+import java.util.Locale.LanguageRange;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -111,6 +113,7 @@
import org.springdoc.core.utils.PropertyResolverUtils;
import org.springdoc.core.utils.SpringDocAnnotationsUtils;
import org.springdoc.core.utils.SpringDocUtils;
+import org.springdoc.core.versions.SpringDocVersionStrategy;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.ObjectFactory;
@@ -369,7 +372,7 @@ protected OpenAPI getOpenApi(String serverBaseUrl, Locale locale) {
.filter(controller -> (AnnotationUtils.findAnnotation(controller.getValue().getClass(),
Hidden.class) == null))
.filter(controller -> !isHiddenRestControllers(controller.getValue().getClass()))
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a1, a2) -> a1));
+ .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (a1, a2) -> a1));
Map findControllerAdvice = openAPIService.getControllerAdviceMap();
if (OpenApiVersion.OPENAPI_3_1 == springDocConfigProperties.getApiDocs().getVersion()) {
@@ -434,7 +437,7 @@ private Locale selectLocale(Locale inputLocale) {
List allowedLocales = springDocConfigProperties.getAllowedLocales();
if (!CollectionUtils.isEmpty(allowedLocales)) {
Locale bestMatchingAllowedLocale = Locale.lookup(
- Locale.LanguageRange.parse(inputLocale.toLanguageTag()),
+ LanguageRange.parse(inputLocale.toLanguageTag()),
allowedLocales.stream().map(Locale::forLanguageTag).toList()
);
@@ -568,7 +571,9 @@ protected void calculatePath(HandlerMethod handlerMethod, RouterOperation router
String[] methodProduces = routerOperation.getProduces();
String[] headers = routerOperation.getHeaders();
Map queryParams = routerOperation.getQueryParams();
-
+ SpringDocVersionStrategy springDocVersionStrategy = routerOperation.getSpringDocVersionStrategy();
+ if (springDocVersionStrategy != null)
+ queryParams = springDocVersionStrategy.updateQueryParams(queryParams);
Components components = openAPI.getComponents();
Paths paths = openAPI.getPaths();
@@ -590,7 +595,7 @@ protected void calculatePath(HandlerMethod handlerMethod, RouterOperation router
RequestMapping reqMappingClass = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(),
RequestMapping.class);
- MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces, headers, locale);
+ MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces, headers, springDocVersionStrategy, locale);
methodAttributes.setMethodOverloaded(existingOperation != null);
//Use the javadoc return if present
if (javadocProvider != null) {
@@ -655,7 +660,7 @@ protected void calculatePath(HandlerMethod handlerMethod, RouterOperation router
}
}
- Set apiCallbacks = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, io.swagger.v3.oas.annotations.callbacks.Callback.class);
+ Set apiCallbacks = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, Callback.class);
// callbacks
buildCallbacks(openAPI, methodAttributes, operation, apiCallbacks);
@@ -757,7 +762,9 @@ protected void calculatePath(RouterOperation routerOperation, Locale locale, Ope
String[] methodProduces = routerOperation.getProduces();
String[] headers = routerOperation.getHeaders();
Map queryParams = routerOperation.getQueryParams();
-
+ SpringDocVersionStrategy springDocVersionStrategy = routerOperation.getSpringDocVersionStrategy();
+ if(springDocVersionStrategy != null)
+ queryParams = springDocVersionStrategy.updateQueryParams(queryParams);
Paths paths = openAPI.getPaths();
Map operationMap = null;
if (paths.containsKey(operationPath)) {
@@ -766,7 +773,7 @@ protected void calculatePath(RouterOperation routerOperation, Locale locale, Ope
}
for (RequestMethod requestMethod : routerOperation.getMethods()) {
Operation existingOperation = getExistingOperation(operationMap, requestMethod);
- MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces, headers, locale);
+ MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces, headers, springDocVersionStrategy, locale);
methodAttributes.setMethodOverloaded(existingOperation != null);
Operation operation = getOperation(routerOperation, existingOperation);
if (apiOperation != null)
@@ -812,19 +819,20 @@ private RouterOperation customizeDataRestRouterOperation(RouterOperation routerO
/**
* Calculate path.
*
- * @param handlerMethod the handler method
- * @param operationPath the operation path
- * @param requestMethods the request methods
- * @param consumes the consumes
- * @param produces the produces
- * @param headers the headers
- * @param params the params
- * @param locale the locale
- * @param openAPI the open api
+ * @param handlerMethod the handler method
+ * @param operationPath the operation path
+ * @param requestMethods the request methods
+ * @param consumes the consumes
+ * @param produces the produces
+ * @param headers the headers
+ * @param params the params
+ * @param versionStrategy the version strategy
+ * @param locale the locale
+ * @param openAPI the open api
*/
protected void calculatePath(HandlerMethod handlerMethod, String operationPath,
- Set requestMethods, String[] consumes, String[] produces, String[] headers, String[] params, Locale locale, OpenAPI openAPI) {
- this.calculatePath(handlerMethod, new RouterOperation(operationPath, requestMethods.toArray(new RequestMethod[requestMethods.size()]), consumes, produces, headers, params), locale, openAPI);
+ Set requestMethods, String[] consumes, String[] produces, String[] headers, String[] params, SpringDocVersionStrategy versionStrategy, Locale locale, OpenAPI openAPI) {
+ this.calculatePath(handlerMethod, new RouterOperation(operationPath, requestMethods.toArray(new RequestMethod[requestMethods.size()]), consumes, produces, headers, params, versionStrategy), locale, openAPI);
}
/**
@@ -1227,10 +1235,16 @@ private void fillParametersList(Operation operation, Map queryPa
}
});
if (!CollectionUtils.isEmpty(queryParams)) {
- for (Map.Entry entry : queryParams.entrySet()) {
- io.swagger.v3.oas.models.parameters.Parameter parameter = new io.swagger.v3.oas.models.parameters.Parameter();
- parameter.setName(entry.getKey());
- parameter.setSchema(new StringSchema()._default(entry.getValue()));
+ Map versionDefaultMap = null;
+ if(methodAttributes.getSpringDocVersionStrategy() != null)
+ versionDefaultMap= methodAttributes.getSpringDocVersionStrategy().getVersionDefaultMap();
+ for (Entry entry : queryParams.entrySet()) {
+ Parameter parameter = new Parameter();
+ String name = entry.getKey();
+ String value = entry.getValue();
+ String defaultValue = (versionDefaultMap != null) ? versionDefaultMap.get(name) : value;
+ parameter.setName(name);
+ parameter.setSchema(new StringSchema()._default(defaultValue)._enum(Collections.singletonList(value)));
parameter.setRequired(true);
parameter.setIn(ParameterIn.QUERY.toString());
GenericParameterService.mergeParameter(parametersList, parameter);
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 2b8882808..e01cd6e00 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
@@ -736,6 +736,6 @@ MethodParameterPojoExtractor methodParameterPojoExtractor(SchemaUtils schemaUtil
@ConditionalOnMissingBean(name = "springDocAppInitializer")
@Lazy(false)
SpringDocAppInitializer springDocAppInitializer(SpringDocConfigProperties springDocConfigProperties){
- return new SpringDocAppInitializer(springDocConfigProperties.getApiDocs().getPath(), SPRINGDOC_ENABLED);
+ return new SpringDocAppInitializer(springDocConfigProperties.getApiDocs().getPath(), SPRINGDOC_ENABLED, springDocConfigProperties.getApiDocs().isEnabled());
}
}
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 add34935f..12a12fede 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
@@ -54,11 +54,11 @@
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
+import org.springframework.data.core.TypeInformation;
import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer;
import org.springframework.data.querydsl.binding.QuerydslBindings;
import org.springframework.data.querydsl.binding.QuerydslBindingsFactory;
import org.springframework.data.querydsl.binding.QuerydslPredicate;
-import org.springframework.data.util.TypeInformation;
import org.springframework.util.CollectionUtils;
import org.springframework.web.method.HandlerMethod;
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/data/DataRestRouterOperationService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/data/DataRestRouterOperationService.java
index bd37f965a..7e2d1c95b 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/data/DataRestRouterOperationService.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/data/DataRestRouterOperationService.java
@@ -285,7 +285,7 @@ else if (ControllerType.PROPERTY.equals(controllerType))
MethodResourceMapping methodResourceMapping, HandlerMethod handlerMethod,
RequestMethod requestMethod, ResourceMetadata resourceMetadata, String
operationPath, ControllerType controllerType) {
- RouterOperation routerOperation = new RouterOperation(operationPath, new RequestMethod[] { requestMethod }, null, null, null, null);
+ RouterOperation routerOperation = new RouterOperation(operationPath, new RequestMethod[] { requestMethod }, null, null, null, null,null);
MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), dataRestRepository.getLocale());
methodAttributes.calculateConsumesProduces(handlerMethod.getMethod());
routerOperation.setConsumes(methodAttributes.getMethodConsumes());
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/events/SpringDocAppInitializer.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/events/SpringDocAppInitializer.java
index f1a02b05b..83fa21557 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/events/SpringDocAppInitializer.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/events/SpringDocAppInitializer.java
@@ -49,6 +49,11 @@ public class SpringDocAppInitializer {
*/
private final String property;
+ /**
+ * The Springdoc enabled.
+ */
+ private final boolean springdocEnabled;
+
/**
* The constant LOGGER.
*/
@@ -57,12 +62,14 @@ public class SpringDocAppInitializer {
/**
* Instantiates a new Spring doc app initializer.
*
- * @param endpoint the endpoint
- * @param property the property
+ * @param endpoint the endpoint
+ * @param property the property
+ * @param springdocEnabled the springdoc enabled
*/
- public SpringDocAppInitializer(String endpoint, String property) {
+ public SpringDocAppInitializer(String endpoint, String property, boolean springdocEnabled) {
this.endpoint = endpoint;
this.property = property;
+ this.springdocEnabled = springdocEnabled;
}
/**
@@ -70,6 +77,7 @@ public SpringDocAppInitializer(String endpoint, String property) {
*/
@EventListener(ApplicationReadyEvent.class)
public void init() {
- LOGGER.warn("SpringDoc {} endpoint is enabled by default. To disable it in production, set the property '{}=false'", endpoint, property);
+ if(!this.springdocEnabled)
+ LOGGER.warn("SpringDoc {} endpoint is enabled by default. To disable it in production, set the property '{}=false'", endpoint, property);
}
}
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/fn/RouterOperation.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/fn/RouterOperation.java
index 693547609..cdec2edaa 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/fn/RouterOperation.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/fn/RouterOperation.java
@@ -35,6 +35,7 @@
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.fn.builders.operation.Builder;
+import org.springdoc.core.versions.SpringDocVersionStrategy;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -107,6 +108,11 @@ public class RouterOperation implements Comparable {
*/
private io.swagger.v3.oas.models.Operation operationModel;
+ /**
+ * The Spring doc version strategy.
+ */
+ private SpringDocVersionStrategy springDocVersionStrategy;
+
/**
* Instantiates a new Router operation.
*/
@@ -154,20 +160,22 @@ public RouterOperation(org.springdoc.core.annotations.RouterOperation routerOper
/**
* Instantiates a new Router operation.
*
- * @param path the path
- * @param methods the methods
- * @param consumes the consumes
- * @param produces the produces
- * @param headers the headers
- * @param params the params
- */
- public RouterOperation(String path, RequestMethod[] methods, String[] consumes, String[] produces, String[] headers, String[] params) {
+ * @param path the path
+ * @param methods the methods
+ * @param consumes the consumes
+ * @param produces the produces
+ * @param headers the headers
+ * @param params the params
+ * @param springDocVersionStrategy the version strategy
+ */
+ public RouterOperation(String path, RequestMethod[] methods, String[] consumes, String[] produces, String[] headers, String[] params, SpringDocVersionStrategy springDocVersionStrategy) {
this.path = path;
this.methods = methods;
this.consumes = consumes;
this.produces = produces;
this.headers = headers;
this.params = params;
+ this.springDocVersionStrategy = springDocVersionStrategy;
}
/**
@@ -422,6 +430,24 @@ public void setOperationModel(io.swagger.v3.oas.models.Operation operationModel)
this.operationModel = operationModel;
}
+ /**
+ * Gets version strategy.
+ *
+ * @return the version strategy
+ */
+ public SpringDocVersionStrategy getSpringDocVersionStrategy() {
+ return springDocVersionStrategy;
+ }
+
+ /**
+ * Sets version strategy.
+ *
+ * @param springDocVersionStrategy the version strategy
+ */
+ public void setVersionStrategy(SpringDocVersionStrategy springDocVersionStrategy) {
+ this.springDocVersionStrategy = springDocVersionStrategy;
+ }
+
@Override
public int compareTo(RouterOperation routerOperation) {
int result = path.compareTo(routerOperation.getPath());
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/MethodAttributes.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/MethodAttributes.java
index 8baac19d9..59e6da6df 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/MethodAttributes.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/MethodAttributes.java
@@ -41,6 +41,9 @@
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.Nullable;
+import org.springdoc.core.versions.HeaderVersionStrategy;
+import org.springdoc.core.versions.MediaTypeVersionStrategy;
+import org.springdoc.core.versions.SpringDocVersionStrategy;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -136,6 +139,11 @@ public class MethodAttributes {
*/
private boolean useReturnTypeSchema;
+ /**
+ * The Spring doc version strategy.
+ */
+ private SpringDocVersionStrategy springDocVersionStrategy;
+
/**
* Instantiates a new Method attributes.
*
@@ -174,14 +182,17 @@ public MethodAttributes(String defaultConsumesMediaType, String defaultProducesM
* @param methodConsumes the method consumes
* @param methodProduces the method produces
* @param headers the headers
+ * @param springDocVersionStrategy the spring doc version strategy
* @param locale the locale
*/
- public MethodAttributes(String defaultConsumesMediaType, String defaultProducesMediaType, String[] methodConsumes, String[] methodProduces, String[] headers, Locale locale) {
+ public MethodAttributes(String defaultConsumesMediaType, String defaultProducesMediaType, String[] methodConsumes, String[] methodProduces, String[] headers,
+ SpringDocVersionStrategy springDocVersionStrategy, Locale locale) {
this.defaultConsumesMediaType = defaultConsumesMediaType;
this.defaultProducesMediaType = defaultProducesMediaType;
this.methodProduces = methodProduces;
this.methodConsumes = methodConsumes;
this.locale = locale;
+ this.springDocVersionStrategy = springDocVersionStrategy;
setHeaders(headers);
}
@@ -296,6 +307,10 @@ else if (reqMappingClass != null) {
private void fillMethods(String[] produces, String[] consumes, String[] headers) {
if (ArrayUtils.isNotEmpty(produces)) {
methodProduces = mergeArrays(methodProduces, produces);
+ if (springDocVersionStrategy instanceof MediaTypeVersionStrategy mediaTypeVersionStrategy
+ && mediaTypeVersionStrategy.getVersion() != null) {
+ methodProduces = mediaTypeVersionStrategy.buildProduces();
+ }
}
else if (ArrayUtils.isNotEmpty(classProduces)) {
methodProduces = mergeArrays(methodProduces, classProduces);
@@ -318,13 +333,13 @@ else if (ArrayUtils.isEmpty(methodConsumes)) {
}
/**
- * If there is any method type(s) present, then these will override the class type(s).
- * See ... for details
+ * If there is any method type(s) present, then these will override the class type(s).
+ * See ... for details
*
- * @param methodTypes the method types
- * @param classTypes the class types
- * @return the string [ ] containing the types that can be used for the method
- */
+ * @param methodTypes the method types
+ * @param classTypes the class types
+ * @return the string [ ] containing the types that can be used for the method
+ */
private String[] calculateMethodMediaTypes(@Nullable String[] methodTypes, String[] classTypes) {
if (ArrayUtils.isNotEmpty(methodTypes)) {
return methodTypes;
@@ -340,9 +355,45 @@ private String[] calculateMethodMediaTypes(@Nullable String[] methodTypes, Strin
* @return the string [ ]
*/
private String[] mergeArrays(@Nullable String[] array1, String[] array2) {
- Set uniqueValues = array1 == null ? new LinkedHashSet<>() : Arrays.stream(array1).collect(Collectors.toCollection(LinkedHashSet::new));
- uniqueValues.addAll(Arrays.asList(array2));
- return uniqueValues.toArray(new String[0]);
+ Set merged = array1 == null
+ ? new LinkedHashSet<>()
+ : Arrays.stream(array1).collect(Collectors.toCollection(LinkedHashSet::new));
+
+ merged.addAll(Arrays.asList(array2));
+
+ // Apply dynamic version filtering if versionStrategy is active
+ if (springDocVersionStrategy != null
+ && springDocVersionStrategy instanceof MediaTypeVersionStrategy mediaTypeVersionStrategy
+ && mediaTypeVersionStrategy.getVersion() != null) {
+
+ // Remove unversioned media types if versioned variants exist
+ Set baseTypesWithVersion = merged.stream()
+ .filter(mt -> mt.contains(";"))
+ .filter(this::containsVersionParameter)
+ .map(mt -> mt.split(";", 2)[0])
+ .collect(Collectors.toSet());
+
+ merged.removeIf(mt -> baseTypesWithVersion.contains(mt) && !mt.contains(";"));
+ }
+
+ return merged.toArray(new String[0]);
+ }
+
+ /**
+ * Contains version parameter boolean.
+ *
+ * @param mediaType the media type
+ * @return the boolean
+ */
+ private boolean containsVersionParameter(String mediaType) {
+ String[] parts = mediaType.split(";");
+ for (int i = 1; i < parts.length; i++) {
+ String param = parts[i].trim().toLowerCase();
+ if (param.startsWith("version=") || param.startsWith("v=") || param.contains("version=")) {
+ return true;
+ }
+ }
+ return false;
}
/**
@@ -419,6 +470,15 @@ public void setJsonViewAnnotationForRequestBody(JsonView jsonViewAnnotationForRe
this.jsonViewAnnotationForRequestBody = jsonViewAnnotationForRequestBody;
}
+ /**
+ * Is with response body schema doc boolean.
+ *
+ * @return the boolean
+ */
+ public boolean isWithResponseBodySchemaDoc() {
+ return withResponseBodySchemaDoc;
+ }
+
/**
* Gets headers.
*
@@ -447,6 +507,11 @@ private void setHeaders(String[] headers) {
this.headers.put(keyValueHeader[0], StringUtils.EMPTY);
}
}
+
+ if (springDocVersionStrategy instanceof HeaderVersionStrategy headerVersionStrategy
+ && headerVersionStrategy.getVersion() != null) {
+ this.headers.put(headerVersionStrategy.getHeaderName(), headerVersionStrategy.getVersion());
+ }
}
/**
@@ -471,15 +536,6 @@ public Map getGenericMapResponse() {
return genericMapResponse;
}
- /**
- * Is with response body schema doc boolean.
- *
- * @return the boolean
- */
- public boolean isWithResponseBodySchemaDoc() {
- return withResponseBodySchemaDoc;
- }
-
/**
* Sets with response body schema doc.
*
@@ -545,4 +601,13 @@ public boolean isUseReturnTypeSchema() {
public void setUseReturnTypeSchema(boolean useReturnTypeSchema) {
this.useReturnTypeSchema = useReturnTypeSchema;
}
+
+ /**
+ * Gets spring doc version strategy.
+ *
+ * @return the spring doc version strategy
+ */
+ public SpringDocVersionStrategy getSpringDocVersionStrategy() {
+ return springDocVersionStrategy;
+ }
}
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SpringDocConfigProperties.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SpringDocConfigProperties.java
index d27787f51..1ea92f1ff 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SpringDocConfigProperties.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SpringDocConfigProperties.java
@@ -1434,7 +1434,7 @@ public static class ApiDocs {
/**
* Whether to generate and serve an OpenAPI document.
*/
- private boolean enabled = true;
+ private boolean enabled;
/**
* The Resolve schema properties.
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SwaggerUiConfigProperties.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SwaggerUiConfigProperties.java
index dd3544909..c3e95e6f3 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SwaggerUiConfigProperties.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/SwaggerUiConfigProperties.java
@@ -96,7 +96,7 @@ public class SwaggerUiConfigProperties extends AbstractSwaggerUiConfigProperties
/**
* Whether to generate and serve an OpenAPI document.
*/
- private boolean enabled = true;
+ private boolean enabled;
/**
* The Use root path.
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/providers/SpringWebProvider.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/providers/SpringWebProvider.java
index aebda82b2..71e9e2417 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/providers/SpringWebProvider.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/providers/SpringWebProvider.java
@@ -25,14 +25,21 @@
*/
package org.springdoc.core.providers;
+import java.util.HashMap;
import java.util.Map;
import java.util.Set;
+import org.apache.commons.lang3.ArrayUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springdoc.core.properties.SpringDocConfigProperties;
+import org.springdoc.core.versions.SpringDocApiVersionType;
+import org.springdoc.core.versions.SpringDocVersionStrategy;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
+import org.springframework.util.CollectionUtils;
/**
* The type Spring web provider.
@@ -41,6 +48,11 @@
*/
public abstract class SpringWebProvider implements ApplicationContextAware {
+ /**
+ * The constant LOGGER.
+ */
+ protected static final Logger LOGGER = LoggerFactory.getLogger(SpringWebProvider.class);
+
/**
* The Application context.
*/
@@ -51,6 +63,11 @@ public abstract class SpringWebProvider implements ApplicationContextAware {
*/
protected Map handlerMethods;
+ /**
+ * The Spring doc version strategy map.
+ */
+ protected final Map springDocVersionStrategyMap = new HashMap<>();
+
/**
* Gets handler methods.
*
@@ -74,6 +91,46 @@ public abstract class SpringWebProvider implements ApplicationContextAware {
*/
public abstract Set getActivePatterns(Object requestMappingInfo);
+ /**
+ * Gets spring doc version strategy.
+ *
+ * @param version the version
+ * @param params the params
+ * @return the spring doc version strategy
+ */
+ public SpringDocVersionStrategy getSpringDocVersionStrategy(String version, String[] params) {
+ SpringDocVersionStrategy springDocVersionStrategy = null;
+ if (!CollectionUtils.isEmpty(springDocVersionStrategyMap)) {
+ if (springDocVersionStrategyMap.size() == 1)
+ springDocVersionStrategy = springDocVersionStrategyMap.values().iterator().next();
+ else
+ springDocVersionStrategy = resolveApiVersionStrategy(version, params);
+ springDocVersionStrategy.updateVersion(version, params);
+ }
+ return springDocVersionStrategy;
+ }
+
+ /**
+ * Resolve api version strategy spring doc version strategy.
+ *
+ * @param version the version
+ * @param params the params
+ * @return the spring doc version strategy
+ */
+ private SpringDocVersionStrategy resolveApiVersionStrategy(String version, String[] params) {
+ if (version != null) {
+ if (springDocVersionStrategyMap.containsKey(SpringDocApiVersionType.PATH))
+ return springDocVersionStrategyMap.get(SpringDocApiVersionType.PATH);
+ else if (springDocVersionStrategyMap.containsKey(SpringDocApiVersionType.HEADER))
+ return springDocVersionStrategyMap.get(SpringDocApiVersionType.HEADER);
+ else if (springDocVersionStrategyMap.containsKey(SpringDocApiVersionType.MEDIA_TYPE))
+ return springDocVersionStrategyMap.get(SpringDocApiVersionType.MEDIA_TYPE);
+ }
+ if (ArrayUtils.isNotEmpty(params) && springDocVersionStrategyMap.containsKey(SpringDocApiVersionType.QUERY_PARAM))
+ return springDocVersionStrategyMap.get(SpringDocApiVersionType.QUERY_PARAM);
+ return springDocVersionStrategyMap.values().iterator().next();
+ }
+
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java
index 0e6dcfe00..0dce0206f 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java
@@ -72,6 +72,7 @@
import org.slf4j.LoggerFactory;
import org.springdoc.core.customizers.DelegatingMethodParameterCustomizer;
import org.springdoc.core.customizers.ParameterCustomizer;
+import org.springdoc.core.customizers.PropertyCustomizer;
import org.springdoc.core.customizers.SpringDocCustomizers;
import org.springdoc.core.discoverer.SpringDocParameterNameDiscoverer;
import org.springdoc.core.extractor.DelegatingMethodParameter;
@@ -124,7 +125,7 @@ public abstract class AbstractRequestService {
* The constant LOGGER.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRequestService.class);
-
+
/**
* The constant ACTUATOR_PKGS.
*/
@@ -132,7 +133,7 @@ public abstract class AbstractRequestService {
"org.springframework.boot.webmvc.actuate",
"org.springframework.boot.webflux.actuate"
);
-
+
/**
* The constant PARAM_TYPES_TO_IGNORE.
*/
@@ -264,19 +265,31 @@ public static boolean isRequestTypeToIgnore(Class> rawClass) {
*/
@SuppressWarnings("unchecked")
public static Collection getHeaders(MethodAttributes methodAttributes, Map map) {
+ Map versionDefaultMap = null;
+ if (methodAttributes.getSpringDocVersionStrategy() != null)
+ versionDefaultMap = methodAttributes.getSpringDocVersionStrategy().getVersionDefaultMap();
for (Entry entry : methodAttributes.getHeaders().entrySet()) {
StringSchema schema = new StringSchema();
- if (StringUtils.isNotEmpty(entry.getValue()))
- schema.addEnumItem(entry.getValue());
- Parameter parameter = new Parameter().in(ParameterIn.HEADER.toString()).name(entry.getKey()).schema(schema);
+ String headerName = entry.getKey();
+ String headerValue = entry.getValue();
+ if (StringUtils.isNotEmpty(headerValue))
+ schema.addEnumItem(headerValue);
+ String defaultValue = null;
+ if(versionDefaultMap != null) {
+ defaultValue = versionDefaultMap.get(headerName);
+ schema._default(defaultValue);
+ }
+ Parameter parameter = new Parameter().in(ParameterIn.HEADER.toString()).name(headerName).schema(schema);
ParameterId parameterId = new ParameterId(parameter);
if (map.containsKey(parameterId)) {
parameter = map.get(parameterId);
List existingEnum = null;
if (parameter.getSchema() != null && !CollectionUtils.isEmpty(parameter.getSchema().getEnum()))
existingEnum = parameter.getSchema().getEnum();
- if (StringUtils.isNotEmpty(entry.getValue()) && (existingEnum == null || !existingEnum.contains(entry.getValue())))
- parameter.getSchema().addEnumItemObject(entry.getValue());
+ if (StringUtils.isNotEmpty(headerValue) && (existingEnum == null || !existingEnum.contains(headerValue)))
+ parameter.getSchema().addEnumItemObject(headerValue);
+ if (defaultValue != null && (existingEnum == null || !existingEnum.contains(defaultValue)))
+ parameter.getSchema().addEnumItemObject(defaultValue);
parameter.setSchema(parameter.getSchema());
}
map.put(parameterId, parameter);
@@ -293,9 +306,9 @@ public static Collection getHeaders(MethodAttributes methodAttributes
* @param methodAttributes the method attributes
* @param openAPI the open api
* @return the operation
- * @see org.springdoc.core.customizers.DelegatingMethodParameterCustomizer#customizeList(MethodParameter, List) org.springdoc.core.customizers.DelegatingMethodParameterCustomizer#customizeList(MethodParameter, List)
+ * @see DelegatingMethodParameterCustomizer#customizeList(MethodParameter, List) org.springdoc.core.customizers.DelegatingMethodParameterCustomizer#customizeList(MethodParameter, List)
* @see ParameterCustomizer#customize(Parameter, MethodParameter) ParameterCustomizer#customize(Parameter, MethodParameter)
- * @see org.springdoc.core.customizers.PropertyCustomizer#customize(Schema, AnnotatedType) org.springdoc.core.customizers.PropertyCustomizer#customize(Schema, AnnotatedType)
+ * @see PropertyCustomizer#customize(Schema, AnnotatedType) org.springdoc.core.customizers.PropertyCustomizer#customize(Schema, AnnotatedType)
*/
public Operation build(HandlerMethod handlerMethod, RequestMethod requestMethod,
Operation operation, MethodAttributes methodAttributes, OpenAPI openAPI) {
@@ -530,7 +543,7 @@ private boolean isRequestBodyWithMapType(MethodParameter parameter) {
if (ACTUATOR_PKGS.stream().anyMatch(pkg::startsWith)) {
return false;
}
-
+
// Check for @RequestBody annotation
org.springframework.web.bind.annotation.RequestBody requestBody = parameter.getParameterAnnotation(org.springframework.web.bind.annotation.RequestBody.class);
if (requestBody == null) {
@@ -549,7 +562,7 @@ private boolean isRequestBodyWithMapType(MethodParameter parameter) {
*/
private boolean isRequestPartWithMapType(MethodParameter parameter) {
// Check for @RequestPart annotation
- org.springframework.web.bind.annotation.RequestPart requestPart = parameter.getParameterAnnotation(org.springframework.web.bind.annotation.RequestPart.class);
+ RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
if (requestPart == null) {
return false;
}
@@ -693,7 +706,7 @@ public void applyBeanValidatorAnnotations(final MethodParameter methodParameter,
java.lang.reflect.AnnotatedType[] typeArgs = paramType.getAnnotatedActualTypeArguments();
for (java.lang.reflect.AnnotatedType typeArg : typeArgs) {
List genericAnnotations = Arrays.stream(typeArg.getAnnotations()).toList();
- Schema schemaItemsClone = cloneViaJson(schema.getItems(), Schema.class, ObjectMapperProvider.createJson(parameterBuilder.getPropertyResolverUtils().getSpringDocConfigProperties()));
+ Schema schemaItemsClone = cloneViaJson(schema.getItems(), Schema.class, ObjectMapperProvider.createJson(parameterBuilder.getPropertyResolverUtils().getSpringDocConfigProperties()));
schema.items(schemaItemsClone);
SchemaUtils.applyValidationsToSchema(schema.getItems(), genericAnnotations, openapiVersion);
}
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericParameterService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericParameterService.java
index ebb63f801..4d04070ce 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericParameterService.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericParameterService.java
@@ -57,6 +57,7 @@
import io.swagger.v3.oas.models.media.FileSchema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.Parameter;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
@@ -237,7 +238,7 @@ public static void mergeParameter(Parameter paramCalcul, Parameter paramDoc) {
paramDoc.setAllowReserved(paramCalcul.getAllowReserved());
if (StringUtils.isBlank(paramDoc.get$ref()))
- paramDoc.set$ref(paramDoc.get$ref());
+ paramDoc.set$ref(paramCalcul.get$ref());
if (paramDoc.getSchema() == null && paramDoc.getContent() == null)
paramDoc.setSchema(paramCalcul.getSchema());
@@ -253,6 +254,29 @@ public static void mergeParameter(Parameter paramCalcul, Parameter paramDoc) {
if (paramDoc.getExplode() == null)
paramDoc.setExplode(paramCalcul.getExplode());
+
+ if (paramDoc.getSchema() instanceof StringSchema existingSchema &&
+ paramCalcul.getSchema() instanceof StringSchema newSchema) {
+
+ List existingEnums = existingSchema.getEnum() != null
+ ? new ArrayList<>(existingSchema.getEnum())
+ : new ArrayList<>();
+
+ List newEnums = newSchema.getEnum();
+
+ if (newEnums != null && !newEnums.isEmpty()) {
+ for (String val : newEnums) {
+ if (!existingEnums.contains(val)) {
+ existingEnums.add(val);
+ }
+ }
+ existingSchema.setEnum(existingEnums);
+ }
+
+ if (newSchema.getDefault() != null) {
+ existingSchema.setDefault(newSchema.getDefault());
+ }
+ }
}
/**
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/RequestBodyService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/RequestBodyService.java
index 35ec79e9b..1b8bdd309 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/RequestBodyService.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/RequestBodyService.java
@@ -299,7 +299,9 @@ private RequestBody buildRequestBody(io.swagger.v3.oas.annotations.parameters.Re
Schema> schema = parameterBuilder.calculateSchema(components, parameterInfo, requestBodyInfo,
methodAttributes.getJsonViewAnnotationForRequestBody());
Map parameterEncoding = getParameterEncoding(parameterInfo);
- buildContent(requestBody, methodAttributes, schema, parameterEncoding);
+ // If a content type is explicitly stated with a @RequestBody annotation, yield to that content type
+ if (!methodAttributes.isWithResponseBodySchemaDoc())
+ buildContent(requestBody, methodAttributes, schema, parameterEncoding);
// Add requestBody javadoc
if (StringUtils.isBlank(requestBody.getDescription()) && parameterBuilder.getJavadocProvider() != null
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 6ffef2802..43f3c9870 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
@@ -52,6 +52,7 @@
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
+import org.springframework.data.core.TypeInformation;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.SimpleAssociationHandler;
@@ -61,7 +62,6 @@
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.webmvc.RestMediaTypes;
-import org.springframework.data.util.TypeInformation;
import org.springframework.hateoas.server.LinkRelationProvider;
import org.springframework.util.CollectionUtils;
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/HeaderVersionStrategy.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/HeaderVersionStrategy.java
new file mode 100644
index 000000000..28895bc93
--- /dev/null
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/HeaderVersionStrategy.java
@@ -0,0 +1,61 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2025 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.versions;
+
+/**
+ * The type Header version strategy.
+ *
+ * @author bnasslahsen
+ */
+public class HeaderVersionStrategy extends SpringDocVersionStrategy {
+
+ /**
+ * The Header name.
+ */
+ private final String headerName;
+
+
+ /**
+ * Instantiates a new Header version strategy.
+ *
+ * @param headerName the header name
+ */
+ public HeaderVersionStrategy(String headerName, String defaultVersion) {
+ super(defaultVersion);
+ this.headerName = headerName;
+ if(defaultVersion != null)
+ versionDefaultMap.put(headerName, defaultVersion);
+ }
+
+ /**
+ * Gets header name.
+ *
+ * @return the header name
+ */
+ public String getHeaderName() {
+ return headerName;
+ }
+}
\ No newline at end of file
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/MediaTypeVersionStrategy.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/MediaTypeVersionStrategy.java
new file mode 100644
index 000000000..f43e5406a
--- /dev/null
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/MediaTypeVersionStrategy.java
@@ -0,0 +1,91 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2025 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.versions;
+
+import org.springframework.http.MediaType;
+
+/**
+ * The type Media type version strategy.
+ *
+ * @author bnasslahsen
+ */
+public class MediaTypeVersionStrategy extends SpringDocVersionStrategy {
+
+ /**
+ * The Media type.
+ */
+ private final MediaType mediaType;
+
+ /**
+ * The Parameter name.
+ */
+ private final String parameterName;
+
+ /**
+ * Instantiates a new Media type version strategy.
+ *
+ * @param mediaType the media type
+ * @param parameterName the parameter name
+ */
+ public MediaTypeVersionStrategy(MediaType mediaType, String parameterName, String defaultVersion) {
+ super(defaultVersion);
+ this.mediaType = mediaType;
+ this.parameterName = parameterName;
+ if(defaultVersion != null)
+ versionDefaultMap.put(parameterName, defaultVersion);
+ }
+
+ /**
+ * Gets media type.
+ *
+ * @return the media type
+ */
+ public MediaType getMediaType() {
+ return mediaType;
+ }
+
+ /**
+ * Gets parameter name.
+ *
+ * @return the parameter name
+ */
+ public String getParameterName() {
+ return parameterName;
+ }
+
+ /**
+ * Build produces string [ ].
+ *
+ * @return the string [ ]
+ */
+ public String[] buildProduces() {
+ String type = mediaType.getType();
+ String subtype = mediaType.getSubtype();
+ String produces = String.format("%s/%s;%s=%s", type, subtype, parameterName, version);
+ return new String[] { produces };
+ }
+
+}
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/PathVersionStrategy.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/PathVersionStrategy.java
new file mode 100644
index 000000000..495d96ea2
--- /dev/null
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/PathVersionStrategy.java
@@ -0,0 +1,97 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2025 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.versions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The type Path version strategy.
+ *
+ * @author bnasslahsen
+ */
+public class PathVersionStrategy extends SpringDocVersionStrategy {
+
+ /**
+ * The Path segment index.
+ */
+ private final int pathSegmentIndex;
+
+ /**
+ * Instantiates a new Path version strategy.
+ *
+ * @param pathSegmentIndex the path segment index
+ */
+ public PathVersionStrategy(int pathSegmentIndex, String defaultVersion) {
+ super(defaultVersion);
+ this.pathSegmentIndex = pathSegmentIndex;
+ }
+
+ /**
+ * Gets path segment index.
+ *
+ * @return the path segment index
+ */
+ public int getPathSegmentIndex() {
+ return pathSegmentIndex;
+ }
+
+
+ @Override
+ public String updateOperationPath(String operationPath, String version) {
+ if (operationPath == null || version == null) {
+ return operationPath;
+ }
+ String[] segments = operationPath.split("/");
+ List updatedSegments = new ArrayList<>();
+ int segmentCount = 0;
+
+ for (String segment : segments) {
+ if (segment.isEmpty()) {
+ continue;
+ }
+
+ if (segmentCount == pathSegmentIndex) {
+ String newSegment = version;
+ if (segment.contains("{")) {
+ // Extract static prefix before placeholder
+ int placeholderStart = segment.indexOf('{');
+ String prefix = segment.substring(0, placeholderStart);
+ newSegment = prefix + version;
+ }
+ updatedSegments.add(newSegment);
+ } else {
+ updatedSegments.add(segment);
+ }
+ segmentCount++;
+ }
+ return "/" + String.join("/", updatedSegments);
+ }
+
+}
+
+
+
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/QueryParamVersionStrategy.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/QueryParamVersionStrategy.java
new file mode 100644
index 000000000..03eef82d2
--- /dev/null
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/QueryParamVersionStrategy.java
@@ -0,0 +1,88 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2025 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.versions;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * The type Query param version strategy.
+ *
+ * @author bnasslahsen
+ */
+public class QueryParamVersionStrategy extends SpringDocVersionStrategy {
+
+ /**
+ * The Parameter name.
+ */
+ private final String parameterName;
+
+ /**
+ * Instantiates a new Query param version strategy.
+ *
+ * @param parameterName the parameter name
+ */
+ public QueryParamVersionStrategy(String parameterName, String defaultVersion) {
+ super(defaultVersion);
+ this.parameterName = parameterName;
+ if(defaultVersion != null)
+ versionDefaultMap.put(parameterName, defaultVersion);
+ }
+
+ /**
+ * Gets parameter name.
+ *
+ * @return the parameter name
+ */
+ public String getParameterName() {
+ return parameterName;
+ }
+
+
+ @Override
+ public void updateVersion(String version, String[] params) {
+ for (String param : params) {
+ if (param.contains("=")) {
+ String[] paramValues = param.split("=", 2);
+ String paramName = paramValues[0];
+ String paramValue = paramValues[1];
+ if (parameterName.equals(paramName)) {
+ setVersion(paramValue);
+ break;
+ }
+ }
+ }
+ }
+
+ @Override
+ public Map updateQueryParams(Map queryParams) {
+ if (queryParams == null)
+ queryParams = new LinkedHashMap<>();
+ if(version !=null)
+ queryParams.put(parameterName, version);
+ return queryParams;
+ }
+}
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/SpringDocApiVersionType.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/SpringDocApiVersionType.java
new file mode 100644
index 000000000..5afbb1204
--- /dev/null
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/SpringDocApiVersionType.java
@@ -0,0 +1,33 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2025 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.versions;
+
+/**
+ * @author bnasslahsen
+ */
+public enum SpringDocApiVersionType {
+ PATH, HEADER, QUERY_PARAM, MEDIA_TYPE
+}
\ No newline at end of file
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/SpringDocVersionStrategy.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/SpringDocVersionStrategy.java
new file mode 100644
index 000000000..91d1482dc
--- /dev/null
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/SpringDocVersionStrategy.java
@@ -0,0 +1,114 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2025 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.versions;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The interface Spring doc version strategy.
+ *
+ * @author bnasslahsen
+ */
+public abstract class SpringDocVersionStrategy {
+
+ /**
+ * The Version.
+ */
+ protected String version;
+
+ /**
+ * The Default version.
+ */
+ protected final String defaultVersion;
+
+ /**
+ * The Default values.
+ */
+ protected Map versionDefaultMap = new HashMap<>();
+
+ /**
+ * Instantiates a new Spring doc version strategy.
+ *
+ * @param defaultVersion the default version
+ */
+ protected SpringDocVersionStrategy(String defaultVersion) {
+ this.defaultVersion = defaultVersion;
+ }
+
+ /**
+ * Gets version.
+ *
+ * @return the version
+ */
+ public String getVersion() {
+ return version;
+ }
+
+ /**
+ * Sets version.
+ *
+ * @param version the version
+ */
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ /**
+ * Update version.
+ *
+ * @param version the version
+ * @param params the params
+ */
+ public void updateVersion(String version, String[] params) {
+ setVersion(version);
+ }
+
+ /**
+ * Update query params map.
+ *
+ * @param queryParams the query params
+ * @return the map
+ */
+ public Map updateQueryParams(Map queryParams) {
+ return queryParams;
+ }
+
+ /**
+ * Gets operation path.
+ *
+ * @param operationPath the operation path
+ * @param version the version
+ * @return the operation path
+ */
+ public String updateOperationPath(String operationPath, String version) {
+ return operationPath;
+ }
+
+ public Map getVersionDefaultMap() {
+ return versionDefaultMap;
+ }
+}
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/AbstractScalarController.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/AbstractScalarController.java
index 68120fd52..af83251b5 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/AbstractScalarController.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/AbstractScalarController.java
@@ -28,17 +28,18 @@
import java.io.IOException;
import java.io.InputStream;
-import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
-import java.util.List;
-import java.util.stream.Collectors;
import com.scalar.maven.webjar.ScalarProperties;
-import com.scalar.maven.webjar.ScalarProperties.ScalarSource;
+import com.scalar.maven.webjar.internal.ScalarConfiguration;
+import com.scalar.maven.webjar.internal.ScalarConfigurationMapper;
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
+import static org.springdoc.scalar.ScalarConstants.HTML_TEMPLATE_PATH;
import static org.springdoc.scalar.ScalarConstants.SCALAR_DEFAULT_URL;
import static org.springdoc.scalar.ScalarConstants.SCALAR_JS_FILENAME;
import static org.springframework.util.AntPathMatcher.DEFAULT_PATH_SEPARATOR;
@@ -60,14 +61,21 @@ public abstract class AbstractScalarController {
*/
protected final String originalScalarUrl;
+ /**
+ * The Object mapper.
+ */
+ private final ObjectMapper objectMapper;
+
/**
* Instantiates a new Abstract scalar controller.
*
* @param scalarProperties the scalar properties
+ * @param objectMapper the object mapper
*/
- protected AbstractScalarController(ScalarProperties scalarProperties) {
+ protected AbstractScalarController(ScalarProperties scalarProperties, ObjectMapper objectMapper) {
this.scalarProperties = scalarProperties;
this.originalScalarUrl = scalarProperties.getUrl();
+ this.objectMapper = objectMapper;
}
/**
@@ -82,22 +90,18 @@ protected AbstractScalarController(ScalarProperties scalarProperties) {
*/
protected ResponseEntity getDocs(String requestUrl) throws IOException {
// Load the template HTML
- InputStream inputStream = getClass().getResourceAsStream("/META-INF/resources/webjars/scalar/index.html");
+ InputStream inputStream = getClass().getResourceAsStream(HTML_TEMPLATE_PATH);
if (inputStream == null) {
- return ResponseEntity.notFound().build();
+ throw new IOException("HTML template not found at: " + HTML_TEMPLATE_PATH);
}
String html = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
- requestUrl = decode(requestUrl);
+
// Replace the placeholders with actual values
- String cdnUrl = buildJsBundleUrl(requestUrl);
+ String bundleUrl = buildJsBundleUrl(requestUrl);
String injectedHtml = html
- .replace("__JS_BUNDLE_URL__", cdnUrl)
- .replace("__CONFIGURATION__", """
- {
- url: "%s"
- }
- """.formatted(buildApiDocsUrl(requestUrl)));
+ .replace("__JS_BUNDLE_URL__", bundleUrl)
+ .replace("__CONFIGURATION__", buildConfigurationJson(buildApiDocsUrl(requestUrl)));
return ResponseEntity.ok()
.contentType(MediaType.TEXT_HTML)
@@ -126,16 +130,6 @@ protected ResponseEntity getScalarJs() throws IOException {
.body(jsContent);
}
- /**
- * Decode string.
- *
- * @param requestURI the request uri
- * @return the string
- */
- protected String decode(String requestURI) {
- return URLDecoder.decode(requestURI, StandardCharsets.UTF_8);
- }
-
/**
* Gets api docs url.
*
@@ -186,159 +180,20 @@ protected String buildJsBundleUrl(String requestUrl, String scalarPath) {
*/
protected abstract String buildJsBundleUrl(String requestUrl);
- /**
- * Builds the configuration JSON for the Scalar API Reference.
- *
- * @return the configuration JSON as a string
- */
- private String buildConfigurationJson() {
- StringBuilder config = new StringBuilder();
- config.append("{");
-
- // Add URL
- config.append("\n url: \"").append(escapeJson(scalarProperties.getUrl())).append("\"");
-
- // Add sources
- if (scalarProperties.getSources() != null && !scalarProperties.getSources().isEmpty()) {
- config.append(",\n sources: ").append(buildSourcesJsonArray(scalarProperties.getSources()));
- }
-
- // Add showSidebar
- if (!scalarProperties.isShowSidebar()) {
- config.append(",\n showSidebar: false");
- }
-
- // Add hideModels
- if (scalarProperties.isHideModels()) {
- config.append(",\n hideModels: true");
- }
-
- // Add hideTestRequestButton
- if (scalarProperties.isHideTestRequestButton()) {
- config.append(",\n hideTestRequestButton: true");
- }
-
- // Add darkMode
- if (scalarProperties.isDarkMode()) {
- config.append(",\n darkMode: true");
- }
-
- // Add hideDarkModeToggle
- if (scalarProperties.isHideDarkModeToggle()) {
- config.append(",\n hideDarkModeToggle: true");
- }
-
- // Add customCss
- if (scalarProperties.getCustomCss() != null && !scalarProperties.getCustomCss().trim().isEmpty()) {
- config.append(",\n customCss: \"").append(escapeJson(scalarProperties.getCustomCss())).append("\"");
- }
-
- // Add theme
- if (scalarProperties.getTheme() != null && !"default".equals(scalarProperties.getTheme())) {
- config.append(",\n theme: \"").append(escapeJson(scalarProperties.getTheme())).append("\"");
- }
-
- // Add layout
- if (scalarProperties.getLayout() != null && !"modern".equals(scalarProperties.getLayout())) {
- config.append(",\n layout: \"").append(escapeJson(scalarProperties.getLayout())).append("\"");
- }
-
- // Add hideSearch
- if (scalarProperties.isHideSearch()) {
- config.append(",\n hideSearch: true");
- }
-
- // Add documentDownloadType
- if (scalarProperties.getDocumentDownloadType() != null && !"both".equals(scalarProperties.getDocumentDownloadType())) {
- config.append(",\n documentDownloadType: \"").append(escapeJson(scalarProperties.getDocumentDownloadType())).append("\"");
- }
-
- config.append("\n}");
- return config.toString();
- }
-
- /**
- * Escapes a string for JSON output.
+ /**
+ * Build configuration json string.
*
- * @param input the input string
- * @return the escaped string
- */
- private String escapeJson(String input) {
- if (input == null) {
- return "";
- }
- return input.replace("\\", "\\\\")
- .replace("\"", "\\\"")
- .replace("\n", "\\n")
- .replace("\r", "\\r")
- .replace("\t", "\\t");
- }
-
- /**
- * Builds the JSON for the OpenAPI reference sources
- *
- * @param sources list of OpenAPI reference sources
- * @return the sources as a JSON string
- */
- private String buildSourcesJsonArray(List sources) {
- final StringBuilder builder = new StringBuilder("[");
-
- // Filter out sources with invalid urls
- final List filteredSources = sources.stream()
- .filter(source -> isNotNullOrBlank(source.getUrl()))
- .collect(Collectors.toList());
-
- // Append each source to json array
- for (int i = 0; i < filteredSources.size(); i++) {
- final ScalarSource source = filteredSources.get(i);
-
- final String sourceJson = buildSourceJson(source);
- builder.append("\n").append(sourceJson);
-
- if (i != filteredSources.size() - 1) {
- builder.append(",");
- }
- }
-
- builder.append("\n]");
- return builder.toString();
- }
-
- /**
- * Builds the JSON for an OpenAPI reference source
- *
- * @param source the OpenAPI reference source
- * @return the source as a JSON string
+ * @param requestUrl the request url
+ * @return the string
*/
- private String buildSourceJson(ScalarSource source) {
- final StringBuilder builder = new StringBuilder("{");
-
- builder.append("\n url: \"").append(escapeJson(source.getUrl())).append("\"");
-
-
- if (isNotNullOrBlank(source.getTitle())) {
- builder.append(",\n title: \"").append(escapeJson(source.getTitle())).append("\"");
+ private String buildConfigurationJson(String requestUrl) {
+ try {
+ this.scalarProperties.setUrl(requestUrl);
+ ScalarConfiguration config = ScalarConfigurationMapper.map(scalarProperties);
+ return objectMapper.writeValueAsString(config);
}
-
- if (isNotNullOrBlank(source.getSlug())) {
- builder.append(",\n slug: \"").append(escapeJson(source.getSlug())).append("\"");
- }
-
- if (source.isDefault() != null) {
- builder.append(",\n default: ").append(source.isDefault());
+ catch (JacksonException e) {
+ throw new RuntimeException("Failed to serialize Scalar configuration", e);
}
-
- builder.append("\n}");
- return builder.toString();
- }
-
- /**
- * Returns whether a String is not null or blank
- *
- * @param input the string
- * @return whether the string is not null or blank
- */
- private boolean isNotNullOrBlank(String input) {
- return input != null && !input.isBlank();
}
}
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/ScalarConstants.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/ScalarConstants.java
index c694f2132..729e32db7 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/ScalarConstants.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/scalar/ScalarConstants.java
@@ -46,10 +46,16 @@ public class ScalarConstants {
/**
* The constant SCALAR_DEFAULT_URL.
*/
- public static final String SCALAR_DEFAULT_URL = "https://registry.scalar.com/@scalar/apis/galaxy/latest?format=json";
+ public static final String SCALAR_DEFAULT_URL = "https://registry.scalar.com/@scalar/apis/galaxy?format=json";
/**
* The constant DEFAULT_SCALAR_ACTUATOR_PATH.
*/
public static final String DEFAULT_SCALAR_ACTUATOR_PATH = "scalar";
+
+ /**
+ * The constant HTML_TEMPLATE_PATH.
+ */
+ public static final String HTML_TEMPLATE_PATH = "/META-INF/resources/webjars/scalar/index.html";
+
}
diff --git a/springdoc-openapi-starter-webflux-api/pom.xml b/springdoc-openapi-starter-webflux-api/pom.xml
index 899182c4e..df34c8484 100644
--- a/springdoc-openapi-starter-webflux-api/pom.xml
+++ b/springdoc-openapi-starter-webflux-api/pom.xml
@@ -3,7 +3,7 @@
org.springdoc
springdoc-openapi
- 3.0.0-RC1
+ 3.0.0
springdoc-openapi-starter-webflux-api
${project.artifactId}
diff --git a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/api/OpenApiResource.java b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/api/OpenApiResource.java
index 4a04368bc..641bc4b87 100644
--- a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/api/OpenApiResource.java
+++ b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/api/OpenApiResource.java
@@ -49,6 +49,7 @@
import org.springdoc.core.service.GenericResponseService;
import org.springdoc.core.service.OpenAPIService;
import org.springdoc.core.service.OperationService;
+import org.springdoc.core.versions.SpringDocVersionStrategy;
import org.springdoc.webflux.core.visitor.RouterFunctionVisitor;
import reactor.core.publisher.Mono;
@@ -187,13 +188,17 @@ protected void calculatePath(Map restControllers, Map requestMethods = requestMappingInfo.getMethodsCondition().getMethods();
// default allowed requestmethods
if (requestMethods.isEmpty())
requestMethods = this.getDefaultAllowedHttpMethods();
- calculatePath(handlerMethod, operationPath, requestMethods, consumes, produces, headers, params, locale, openAPI);
+ SpringDocVersionStrategy springDocVersionStrategy = springWebProvider.getSpringDocVersionStrategy(version, params);
+ if(springDocVersionStrategy != null)
+ operationPath = springDocVersionStrategy.updateOperationPath(operationPath, version);
+ calculatePath(handlerMethod, operationPath, requestMethods, consumes, produces, headers, params,springDocVersionStrategy, locale, openAPI);
}
}
}
diff --git a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/configuration/SpringDocWebFluxConfiguration.java b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/configuration/SpringDocWebFluxConfiguration.java
index 51cf71e4b..832b1281c 100644
--- a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/configuration/SpringDocWebFluxConfiguration.java
+++ b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/configuration/SpringDocWebFluxConfiguration.java
@@ -65,6 +65,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
+import org.springframework.web.reactive.accept.ApiVersionStrategy;
import static org.springdoc.core.utils.Constants.SPRINGDOC_ENABLED;
@@ -143,13 +144,14 @@ GenericResponseService responseBuilder(OperationService operationService, Spring
/**
* Spring web provider spring web provider.
*
+ * @param apiVersionStrategyOptional the api version strategy optional
* @return the spring web provider
*/
@Bean
@ConditionalOnMissingBean
@Lazy(false)
- SpringWebProvider springWebProvider() {
- return new SpringWebFluxProvider();
+ SpringWebProvider springWebProvider(Optional apiVersionStrategyOptional) {
+ return new SpringWebFluxProvider(apiVersionStrategyOptional);
}
/**
diff --git a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/providers/SpringWebFluxProvider.java b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/providers/SpringWebFluxProvider.java
index 1acca1186..5f8e62875 100644
--- a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/providers/SpringWebFluxProvider.java
+++ b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/providers/SpringWebFluxProvider.java
@@ -25,20 +25,37 @@
*/
package org.springdoc.webflux.core.providers;
+import java.lang.reflect.Field;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.reflect.FieldUtils;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.providers.SpringWebProvider;
+import org.springdoc.core.versions.HeaderVersionStrategy;
+import org.springdoc.core.versions.MediaTypeVersionStrategy;
+import org.springdoc.core.versions.PathVersionStrategy;
+import org.springdoc.core.versions.QueryParamVersionStrategy;
+import org.springdoc.core.versions.SpringDocApiVersionType;
+import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.reactive.accept.ApiVersionResolver;
+import org.springframework.web.reactive.accept.ApiVersionStrategy;
+import org.springframework.web.reactive.accept.DefaultApiVersionStrategy;
+import org.springframework.web.reactive.accept.HeaderApiVersionResolver;
+import org.springframework.web.reactive.accept.MediaTypeParamApiVersionResolver;
+import org.springframework.web.reactive.accept.PathApiVersionResolver;
+import org.springframework.web.reactive.accept.QueryApiVersionResolver;
import org.springframework.web.reactive.result.condition.PatternsRequestCondition;
import org.springframework.web.reactive.result.method.AbstractHandlerMethodMapping;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
@@ -53,6 +70,55 @@
*/
public class SpringWebFluxProvider extends SpringWebProvider {
+ /**
+ * Instantiates a new Spring web flux provider.
+ *
+ * @param apiVersionStrategyOptional the api version strategy optional
+ */
+ public SpringWebFluxProvider(Optional apiVersionStrategyOptional) {
+ apiVersionStrategyOptional.ifPresent(apiVersionStrategy -> {
+ try {
+ DefaultApiVersionStrategy defaultApiVersionStrategy = (DefaultApiVersionStrategy) apiVersionStrategy;
+ String defaultVersion = null;
+ if(defaultApiVersionStrategy.getDefaultVersion() !=null)
+ defaultVersion = defaultApiVersionStrategy.getDefaultVersion().toString();
+ Field field = FieldUtils.getDeclaredField(DefaultApiVersionStrategy.class, "versionResolvers", true);
+ final List versionResolvers = (List) field.get(defaultApiVersionStrategy);
+ for (ApiVersionResolver apiVersionResolver : versionResolvers) {
+ if (apiVersionResolver instanceof MediaTypeParamApiVersionResolver mediaTypeParamApiVersionResolver) {
+ field = FieldUtils.getDeclaredField(MediaTypeParamApiVersionResolver.class, "compatibleMediaType", true);
+ MediaType mediaType = (MediaType) field.get(mediaTypeParamApiVersionResolver);
+ field = FieldUtils.getDeclaredField(MediaTypeParamApiVersionResolver.class, "parameterName", true);
+ String parameterName = (String) field.get(mediaTypeParamApiVersionResolver);
+ MediaTypeVersionStrategy mediaTypeStrategy = new MediaTypeVersionStrategy(mediaType, parameterName, defaultVersion);
+ springDocVersionStrategyMap.put(SpringDocApiVersionType.MEDIA_TYPE, mediaTypeStrategy);
+ }
+ else if (apiVersionResolver instanceof PathApiVersionResolver pathApiVersionResolver) {
+ field = FieldUtils.getDeclaredField(PathApiVersionResolver.class, "pathSegmentIndex", true);
+ Integer pathSegmentIndex = (Integer) field.get(pathApiVersionResolver);
+ PathVersionStrategy pathVersionStrategy = new PathVersionStrategy(pathSegmentIndex, defaultVersion);
+ springDocVersionStrategyMap.put(SpringDocApiVersionType.PATH, pathVersionStrategy);
+ }
+ else if (apiVersionResolver instanceof HeaderApiVersionResolver headerApiVersionResolver) {
+ field = FieldUtils.getDeclaredField(HeaderApiVersionResolver.class, "headerName", true);
+ String headerName = (String) field.get(headerApiVersionResolver);
+ HeaderVersionStrategy headerVersionStrategy = new HeaderVersionStrategy(headerName, defaultVersion);
+ springDocVersionStrategyMap.put(SpringDocApiVersionType.HEADER, headerVersionStrategy);
+ }
+ else if (apiVersionResolver instanceof QueryApiVersionResolver queryApiVersionResolver) {
+ field = FieldUtils.getDeclaredField(QueryApiVersionResolver.class, "queryParamName", true);
+ String queryParamName = (String) field.get(queryApiVersionResolver);
+ QueryParamVersionStrategy queryParamVersionStrategy = new QueryParamVersionStrategy(queryParamName, defaultVersion);
+ springDocVersionStrategyMap.put(SpringDocApiVersionType.QUERY_PARAM, queryParamVersionStrategy);
+ }
+ }
+ }
+ catch (IllegalAccessException e) {
+ LOGGER.warn(e.getMessage());
+ }
+ });
+ }
+
/**
* Finds path prefix.
*
@@ -88,7 +154,6 @@ public Set getActivePatterns(Object requestMapping) {
.map(PathPattern::getPatternString).collect(Collectors.toCollection(LinkedHashSet::new));
}
-
/**
* Gets handler methods.
*
diff --git a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/visitor/RouterFunctionVisitor.java b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/visitor/RouterFunctionVisitor.java
index d3cfb4e5f..756358115 100644
--- a/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/visitor/RouterFunctionVisitor.java
+++ b/springdoc-openapi-starter-webflux-api/src/main/java/org/springdoc/webflux/core/visitor/RouterFunctionVisitor.java
@@ -26,19 +26,24 @@
package org.springdoc.webflux.core.visitor;
+import java.lang.reflect.Field;
import java.util.ArrayList;
+import java.util.Set;
import java.util.function.Function;
import org.springdoc.core.fn.AbstractRouterFunctionVisitor;
import reactor.core.publisher.Mono;
import org.springframework.core.io.Resource;
+import org.springframework.http.HttpMethod;
+import org.springframework.util.ReflectionUtils;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
+import org.springframework.web.util.pattern.PathPattern;
/**
* The type Router function visitor.
@@ -67,7 +72,19 @@ public void endNested(RequestPredicate predicate) {
@Override
public void resources(Function> lookupFunction) {
- // Not yet needed
+ if ("PathResourceLookupFunction".equals(lookupFunction.getClass().getSimpleName())) {
+ Field patternField = ReflectionUtils.findField(lookupFunction.getClass(), "pattern");
+ if (patternField != null) {
+ ReflectionUtils.makeAccessible(patternField);
+ Object patternFieldValue = ReflectionUtils.getField(patternField, lookupFunction);
+ if (patternFieldValue instanceof PathPattern patternCastedValue) {
+ this.currentRouterFunctionDatas = new ArrayList<>();
+ this.path(patternCastedValue.getPatternString());
+ this.commonRoute();
+ this.method(Set.of(HttpMethod.GET));
+ }
+ }
+ }
}
@Override
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java
index 9593f2b80..db8238969 100644
--- a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractCommonTest.java
@@ -35,7 +35,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.webtestclient.AutoConfigureWebTestClient;
+import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.reactive.server.WebTestClient;
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractSpringDocTest.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractSpringDocTest.java
index de0d8b638..26b8ab2d2 100644
--- a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractSpringDocTest.java
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v30/AbstractSpringDocTest.java
@@ -30,7 +30,7 @@
import org.springdoc.core.utils.Constants;
import org.springframework.boot.webflux.test.autoconfigure.WebFluxTest;
-import org.springframework.boot.webtestclient.AutoConfigureWebTestClient;
+import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;
import org.springframework.test.web.reactive.server.EntityExchangeResult;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java
index 3ca0d9afd..3f29f5d89 100644
--- a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractCommonTest.java
@@ -35,7 +35,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.webtestclient.AutoConfigureWebTestClient;
+import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.reactive.server.WebTestClient;
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractSpringDocTest.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractSpringDocTest.java
index 8367ecae5..66cd00188 100644
--- a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractSpringDocTest.java
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/AbstractSpringDocTest.java
@@ -30,7 +30,7 @@
import org.springdoc.core.utils.Constants;
import org.springframework.boot.webflux.test.autoconfigure.WebFluxTest;
-import org.springframework.boot.webtestclient.AutoConfigureWebTestClient;
+import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;
import org.springframework.test.web.reactive.server.EntityExchangeResult;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/SpringDocApp193Test.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/SpringDocApp193Test.java
new file mode 100644
index 000000000..edcafb302
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/SpringDocApp193Test.java
@@ -0,0 +1,39 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2025 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.app193;
+
+import test.org.springdoc.api.v31.AbstractSpringDocTest;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+
+public class SpringDocApp193Test extends AbstractSpringDocTest {
+
+ @SpringBootApplication
+ @ComponentScan(basePackages = { "org.springdoc", "test.org.springdoc.api.v31.app193" })
+ static class SpringDocTestApp {}
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/config/ApiVersionParser.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/config/ApiVersionParser.java
new file mode 100644
index 000000000..166b61383
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/config/ApiVersionParser.java
@@ -0,0 +1,24 @@
+package test.org.springdoc.api.v31.app193.config;
+
+public class ApiVersionParser implements org.springframework.web.accept.ApiVersionParser {
+
+ // allows us to use /api/v2/users instead of /api/2.0/users
+ @Override
+ public Comparable parseVersion(String version) {
+ // Remove "v" prefix if it exists (v1 becomes 1, v2 becomes 2)
+ if (version.startsWith("v") || version.startsWith("V")) {
+ version = version.substring(1);
+ }
+
+ if("api-docs".equals(version) || "index.html".equals(version)
+ || "swagger-ui-bundle.js".equals(version)
+ || "swagger-ui.css".equals(version)
+ || "index.css".equals(version)
+ || "swagger-ui-standalone-preset.js".equals(version)
+ || "favicon-32x32.png".equals(version)
+ || "favicon-16x16.png".equals(version)
+ || "swagger-initializer.js".equals(version))
+ return null;
+ return version;
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/config/WebConfig.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/config/WebConfig.java
new file mode 100644
index 000000000..b0f9dea59
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/config/WebConfig.java
@@ -0,0 +1,23 @@
+package test.org.springdoc.api.v31.app193.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+import org.springframework.web.reactive.config.ApiVersionConfigurer;
+import org.springframework.web.reactive.config.WebFluxConfigurer;
+
+/**
+ * @author bnasslahsen
+ */
+@Configuration
+public class WebConfig implements WebFluxConfigurer {
+
+ @Override
+ public void configureApiVersioning(ApiVersionConfigurer configurer) {
+ configurer
+ .setVersionRequired(false)
+ .addSupportedVersions("1.0","2.0")
+ .setDefaultVersion("1.0")
+ .useMediaTypeParameter(MediaType.APPLICATION_JSON, "version")
+ .setVersionParser(new ApiVersionParser());
+ }
+}
\ No newline at end of file
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/User.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/User.java
new file mode 100644
index 000000000..e28a5b320
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/User.java
@@ -0,0 +1,10 @@
+package test.org.springdoc.api.v31.app193.user;
+
+public record User(
+ Integer id,
+ String name,
+ String email
+
+ // a lot more fields here
+) {
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserController.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserController.java
new file mode 100644
index 000000000..21a7b046f
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserController.java
@@ -0,0 +1,47 @@
+package test.org.springdoc.api.v31.app193.user;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.http.MediaType;
+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 UserController {
+
+ private static final Logger log = LoggerFactory.getLogger(UserController.class);
+ private final UserRepository userRepository;
+ private final UserMapper userMapper;
+
+ public UserController(UserRepository userRepository, UserMapper userMapper) {
+ this.userRepository = userRepository;
+ this.userMapper = userMapper;
+ }
+
+ // USING MEDIA TYPE (Content Negotiation) =======================================
+
+ @GetMapping(value = "/users/media", version = "1.0", produces = "application/json")
+ public List getUsersMediaV1() {
+ log.info("Find All Users using media type versioning: {}", "v1");
+ return userRepository.findAll()
+ .stream()
+ .map(userMapper::toV1)
+ .collect(Collectors.toList());
+ }
+
+ @GetMapping(value = "/users/media", version = "2.0", produces = MediaType.APPLICATION_JSON_VALUE)
+ public List getUsersMediaV2() {
+ log.info("Find All Users using media type versioning: {}", "v2");
+ return userRepository.findAll()
+ .stream()
+ .map(userMapper::toV2)
+ .collect(Collectors.toList());
+ }
+
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserDTOv1.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserDTOv1.java
new file mode 100644
index 000000000..9ba026b48
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserDTOv1.java
@@ -0,0 +1,4 @@
+package test.org.springdoc.api.v31.app193.user;
+
+public record UserDTOv1(Integer id, String name, String email) {
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserDTOv2.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserDTOv2.java
new file mode 100644
index 000000000..ab84172f6
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserDTOv2.java
@@ -0,0 +1,4 @@
+package test.org.springdoc.api.v31.app193.user;
+
+public record UserDTOv2(Integer id, String firstName,String lastName, String email) {
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserMapper.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserMapper.java
new file mode 100644
index 000000000..e1818822f
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserMapper.java
@@ -0,0 +1,83 @@
+package test.org.springdoc.api.v31.app193.user;
+
+import org.springframework.stereotype.Component;
+
+/*
+ * you could also use a library like MapStruct
+ */
+@Component
+public class UserMapper {
+
+ // User to DTOs
+ public UserDTOv1 toV1(User user) {
+ return new UserDTOv1(
+ user.id(),
+ user.name(),
+ user.email()
+ );
+ }
+
+ public UserDTOv2 toV2(User user) {
+ String[] nameParts = splitName(user.name());
+ return new UserDTOv2(
+ user.id(),
+ nameParts[0], // firstName
+ nameParts[1], // lastName
+ user.email()
+ );
+ }
+
+ // DTOs to User
+ public User fromV1(UserDTOv1 dto) {
+ return new User(
+ dto.id(),
+ dto.name(),
+ dto.email()
+ );
+ }
+
+ public User fromV2(UserDTOv2 dto) {
+ String combinedName = combineName(dto.firstName(), dto.lastName());
+ return new User(
+ dto.id(),
+ combinedName,
+ dto.email()
+ );
+ }
+
+ // Helper methods
+ private String[] splitName(String fullName) {
+ if (fullName == null || fullName.trim().isEmpty()) {
+ return new String[]{"", ""};
+ }
+
+ String trimmed = fullName.trim();
+ int lastSpaceIndex = trimmed.lastIndexOf(' ');
+
+ if (lastSpaceIndex == -1) {
+ // Single word name - put it as firstName
+ return new String[]{trimmed, ""};
+ }
+
+ // Split at last space (handles middle names better)
+ // "John Smith Jr" -> "John Smith" and "Jr"
+ // "Mary Ann Smith" -> "Mary Ann" and "Smith"
+ return new String[]{
+ trimmed.substring(0, lastSpaceIndex),
+ trimmed.substring(lastSpaceIndex + 1)
+ };
+ }
+
+ private String combineName(String firstName, String lastName) {
+ firstName = firstName != null ? firstName.trim() : "";
+ lastName = lastName != null ? lastName.trim() : "";
+
+ if (firstName.isEmpty()) {
+ return lastName;
+ }
+ if (lastName.isEmpty()) {
+ return firstName;
+ }
+ return firstName + " " + lastName;
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserRepository.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserRepository.java
new file mode 100644
index 000000000..61eba9c29
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app193/user/UserRepository.java
@@ -0,0 +1,27 @@
+package test.org.springdoc.api.v31.app193.user;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.annotation.PostConstruct;
+
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class UserRepository {
+
+ private final List users = new ArrayList<>();
+
+ public List findAll() {
+ return users;
+ }
+
+ public User findById(Integer id) {
+ return users.stream().filter(u -> u.id().equals(id)).findFirst().orElse(null);
+ }
+
+ @PostConstruct
+ private void init() {
+ users.add(new User(1,"Dan Vega","danvega@gmail.com"));
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/SpringDocApp194Test.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/SpringDocApp194Test.java
new file mode 100644
index 000000000..9788a7cd2
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/SpringDocApp194Test.java
@@ -0,0 +1,39 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2025 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.app194;
+
+import test.org.springdoc.api.v31.AbstractSpringDocTest;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+
+public class SpringDocApp194Test extends AbstractSpringDocTest {
+
+ @SpringBootApplication
+ @ComponentScan(basePackages = { "org.springdoc", "test.org.springdoc.api.v31.app194" })
+ static class SpringDocTestApp {}
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/config/ApiVersionParser.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/config/ApiVersionParser.java
new file mode 100644
index 000000000..78c38fd37
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/config/ApiVersionParser.java
@@ -0,0 +1,24 @@
+package test.org.springdoc.api.v31.app194.config;
+
+public class ApiVersionParser implements org.springframework.web.accept.ApiVersionParser {
+
+ // allows us to use /api/v2/users instead of /api/2.0/users
+ @Override
+ public Comparable parseVersion(String version) {
+ // Remove "v" prefix if it exists (v1 becomes 1, v2 becomes 2)
+ if (version.startsWith("v") || version.startsWith("V")) {
+ version = version.substring(1);
+ }
+
+ if("api-docs".equals(version) || "index.html".equals(version)
+ || "swagger-ui-bundle.js".equals(version)
+ || "swagger-ui.css".equals(version)
+ || "index.css".equals(version)
+ || "swagger-ui-standalone-preset.js".equals(version)
+ || "favicon-32x32.png".equals(version)
+ || "favicon-16x16.png".equals(version)
+ || "swagger-initializer.js".equals(version))
+ return null;
+ return version;
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/config/WebConfig.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/config/WebConfig.java
new file mode 100644
index 000000000..102e18416
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/config/WebConfig.java
@@ -0,0 +1,21 @@
+package test.org.springdoc.api.v31.app194.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.reactive.config.ApiVersionConfigurer;
+import org.springframework.web.reactive.config.WebFluxConfigurer;
+
+@Configuration
+public class WebConfig implements WebFluxConfigurer {
+
+ @Override
+ public void configureApiVersioning(ApiVersionConfigurer configurer) {
+ configurer
+ .usePathSegment(1)
+ .detectSupportedVersions(false)
+ .addSupportedVersions("1.0","2.0")
+ .setDefaultVersion("1.0")
+ .setVersionRequired(false)
+ .setVersionParser(new ApiVersionParser());
+ }
+
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/User.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/User.java
new file mode 100644
index 000000000..1768bf7a3
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/User.java
@@ -0,0 +1,10 @@
+package test.org.springdoc.api.v31.app194.user;
+
+public record User(
+ Integer id,
+ String name,
+ String email
+
+ // a lot more fields here
+) {
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserController.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserController.java
new file mode 100644
index 000000000..21a1ab3b3
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserController.java
@@ -0,0 +1,39 @@
+package test.org.springdoc.api.v31.app194.user;
+
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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 UserController {
+
+ private static final Logger log = LoggerFactory.getLogger(UserController.class);
+ private final UserRepository userRepository;
+ private final UserMapper userMapper;
+
+ public UserController(UserRepository userRepository, UserMapper userMapper) {
+ this.userRepository = userRepository;
+ this.userMapper = userMapper;
+ }
+
+ // USING PATH SEGMENT ======================================================
+
+ @GetMapping(value = "/{version}/users", version = "1.0")
+ public List findAllv1() {
+ log.info("Finding all users v1");
+ return userRepository.findAll();
+ }
+
+ @GetMapping(value = "/{version}/users", version = "2.0")
+ public List findAllv2() {
+ log.info("Finding all users v2");
+ return userRepository.findAll();
+ }
+
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserDTOv1.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserDTOv1.java
new file mode 100644
index 000000000..2a6ea9d64
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserDTOv1.java
@@ -0,0 +1,4 @@
+package test.org.springdoc.api.v31.app194.user;
+
+public record UserDTOv1(Integer id, String name, String email) {
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserDTOv2.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserDTOv2.java
new file mode 100644
index 000000000..e3b381f9d
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserDTOv2.java
@@ -0,0 +1,4 @@
+package test.org.springdoc.api.v31.app194.user;
+
+public record UserDTOv2(Integer id, String firstName,String lastName, String email) {
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserMapper.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserMapper.java
new file mode 100644
index 000000000..a0552e4b4
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserMapper.java
@@ -0,0 +1,83 @@
+package test.org.springdoc.api.v31.app194.user;
+
+import org.springframework.stereotype.Component;
+
+/*
+ * you could also use a library like MapStruct
+ */
+@Component
+public class UserMapper {
+
+ // User to DTOs
+ public UserDTOv1 toV1(User user) {
+ return new UserDTOv1(
+ user.id(),
+ user.name(),
+ user.email()
+ );
+ }
+
+ public UserDTOv2 toV2(User user) {
+ String[] nameParts = splitName(user.name());
+ return new UserDTOv2(
+ user.id(),
+ nameParts[0], // firstName
+ nameParts[1], // lastName
+ user.email()
+ );
+ }
+
+ // DTOs to User
+ public User fromV1(UserDTOv1 dto) {
+ return new User(
+ dto.id(),
+ dto.name(),
+ dto.email()
+ );
+ }
+
+ public User fromV2(UserDTOv2 dto) {
+ String combinedName = combineName(dto.firstName(), dto.lastName());
+ return new User(
+ dto.id(),
+ combinedName,
+ dto.email()
+ );
+ }
+
+ // Helper methods
+ private String[] splitName(String fullName) {
+ if (fullName == null || fullName.trim().isEmpty()) {
+ return new String[]{"", ""};
+ }
+
+ String trimmed = fullName.trim();
+ int lastSpaceIndex = trimmed.lastIndexOf(' ');
+
+ if (lastSpaceIndex == -1) {
+ // Single word name - put it as firstName
+ return new String[]{trimmed, ""};
+ }
+
+ // Split at last space (handles middle names better)
+ // "John Smith Jr" -> "John Smith" and "Jr"
+ // "Mary Ann Smith" -> "Mary Ann" and "Smith"
+ return new String[]{
+ trimmed.substring(0, lastSpaceIndex),
+ trimmed.substring(lastSpaceIndex + 1)
+ };
+ }
+
+ private String combineName(String firstName, String lastName) {
+ firstName = firstName != null ? firstName.trim() : "";
+ lastName = lastName != null ? lastName.trim() : "";
+
+ if (firstName.isEmpty()) {
+ return lastName;
+ }
+ if (lastName.isEmpty()) {
+ return firstName;
+ }
+ return firstName + " " + lastName;
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserRepository.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserRepository.java
new file mode 100644
index 000000000..45d41de23
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app194/user/UserRepository.java
@@ -0,0 +1,27 @@
+package test.org.springdoc.api.v31.app194.user;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.annotation.PostConstruct;
+
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class UserRepository {
+
+ private final List users = new ArrayList<>();
+
+ public List findAll() {
+ return users;
+ }
+
+ public User findById(Integer id) {
+ return users.stream().filter(u -> u.id().equals(id)).findFirst().orElse(null);
+ }
+
+ @PostConstruct
+ private void init() {
+ users.add(new User(1,"Dan Vega","danvega@gmail.com"));
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/SpringDocApp195Test.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/SpringDocApp195Test.java
new file mode 100644
index 000000000..05bbe3f44
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/SpringDocApp195Test.java
@@ -0,0 +1,39 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2025 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.app195;
+
+import test.org.springdoc.api.v31.AbstractSpringDocTest;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+
+public class SpringDocApp195Test extends AbstractSpringDocTest {
+
+ @SpringBootApplication
+ @ComponentScan(basePackages = { "org.springdoc", "test.org.springdoc.api.v31.app195" })
+ static class SpringDocTestApp {}
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/config/ApiVersionParser.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/config/ApiVersionParser.java
new file mode 100644
index 000000000..179d04557
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/config/ApiVersionParser.java
@@ -0,0 +1,24 @@
+package test.org.springdoc.api.v31.app195.config;
+
+public class ApiVersionParser implements org.springframework.web.accept.ApiVersionParser {
+
+ // allows us to use /api/v2/users instead of /api/2.0/users
+ @Override
+ public Comparable parseVersion(String version) {
+ // Remove "v" prefix if it exists (v1 becomes 1, v2 becomes 2)
+ if (version.startsWith("v") || version.startsWith("V")) {
+ version = version.substring(1);
+ }
+
+ if("api-docs".equals(version) || "index.html".equals(version)
+ || "swagger-ui-bundle.js".equals(version)
+ || "swagger-ui.css".equals(version)
+ || "index.css".equals(version)
+ || "swagger-ui-standalone-preset.js".equals(version)
+ || "favicon-32x32.png".equals(version)
+ || "favicon-16x16.png".equals(version)
+ || "swagger-initializer.js".equals(version))
+ return null;
+ return version;
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/config/WebConfig.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/config/WebConfig.java
new file mode 100644
index 000000000..1b6154082
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/config/WebConfig.java
@@ -0,0 +1,21 @@
+package test.org.springdoc.api.v31.app195.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.reactive.config.ApiVersionConfigurer;
+import org.springframework.web.reactive.config.WebFluxConfigurer;
+
+@Configuration
+public class WebConfig implements WebFluxConfigurer {
+
+ @Override
+ public void configureApiVersioning(ApiVersionConfigurer configurer) {
+ configurer
+ .detectSupportedVersions(false)
+ .addSupportedVersions("1.0","2.0")
+ .setDefaultVersion("1.0")
+ .setVersionRequired(false)
+ .useRequestHeader("X-API-Version")
+ .setVersionParser(new ApiVersionParser());
+ }
+
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/User.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/User.java
new file mode 100644
index 000000000..f10aa2e34
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/User.java
@@ -0,0 +1,10 @@
+package test.org.springdoc.api.v31.app195.user;
+
+public record User(
+ Integer id,
+ String name,
+ String email
+
+ // a lot more fields here
+) {
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserController.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserController.java
new file mode 100644
index 000000000..7fe63304a
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserController.java
@@ -0,0 +1,46 @@
+package test.org.springdoc.api.v31.app195.user;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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 UserController {
+
+ private static final Logger log = LoggerFactory.getLogger(UserController.class);
+ private final UserRepository userRepository;
+ private final UserMapper userMapper;
+
+ public UserController(UserRepository userRepository, UserMapper userMapper) {
+ this.userRepository = userRepository;
+ this.userMapper = userMapper;
+ }
+
+ // USING REQUEST HEADER ======================================================
+
+ @GetMapping(value = "/users", version = "1.0")
+ public List getUsersV1() {
+ log.info("Find All Users using request header: {}", "v1");
+ return userRepository.findAll()
+ .stream()
+ .map(userMapper::toV1)
+ .collect(Collectors.toList());
+ }
+
+ @GetMapping(value = "/users", version = "2.0")
+ public List getUsersV2() {
+ log.info("Find All Users using request header: {}", "v2");
+ return userRepository.findAll()
+ .stream()
+ .map(userMapper::toV2)
+ .collect(Collectors.toList());
+ }
+
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserDTOv1.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserDTOv1.java
new file mode 100644
index 000000000..da39f3017
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserDTOv1.java
@@ -0,0 +1,4 @@
+package test.org.springdoc.api.v31.app195.user;
+
+public record UserDTOv1(Integer id, String name, String email) {
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserDTOv2.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserDTOv2.java
new file mode 100644
index 000000000..e381e4c0e
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserDTOv2.java
@@ -0,0 +1,4 @@
+package test.org.springdoc.api.v31.app195.user;
+
+public record UserDTOv2(Integer id, String firstName,String lastName, String email) {
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserMapper.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserMapper.java
new file mode 100644
index 000000000..bdc4a736a
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserMapper.java
@@ -0,0 +1,83 @@
+package test.org.springdoc.api.v31.app195.user;
+
+import org.springframework.stereotype.Component;
+
+/*
+ * you could also use a library like MapStruct
+ */
+@Component
+public class UserMapper {
+
+ // User to DTOs
+ public UserDTOv1 toV1(User user) {
+ return new UserDTOv1(
+ user.id(),
+ user.name(),
+ user.email()
+ );
+ }
+
+ public UserDTOv2 toV2(User user) {
+ String[] nameParts = splitName(user.name());
+ return new UserDTOv2(
+ user.id(),
+ nameParts[0], // firstName
+ nameParts[1], // lastName
+ user.email()
+ );
+ }
+
+ // DTOs to User
+ public User fromV1(UserDTOv1 dto) {
+ return new User(
+ dto.id(),
+ dto.name(),
+ dto.email()
+ );
+ }
+
+ public User fromV2(UserDTOv2 dto) {
+ String combinedName = combineName(dto.firstName(), dto.lastName());
+ return new User(
+ dto.id(),
+ combinedName,
+ dto.email()
+ );
+ }
+
+ // Helper methods
+ private String[] splitName(String fullName) {
+ if (fullName == null || fullName.trim().isEmpty()) {
+ return new String[]{"", ""};
+ }
+
+ String trimmed = fullName.trim();
+ int lastSpaceIndex = trimmed.lastIndexOf(' ');
+
+ if (lastSpaceIndex == -1) {
+ // Single word name - put it as firstName
+ return new String[]{trimmed, ""};
+ }
+
+ // Split at last space (handles middle names better)
+ // "John Smith Jr" -> "John Smith" and "Jr"
+ // "Mary Ann Smith" -> "Mary Ann" and "Smith"
+ return new String[]{
+ trimmed.substring(0, lastSpaceIndex),
+ trimmed.substring(lastSpaceIndex + 1)
+ };
+ }
+
+ private String combineName(String firstName, String lastName) {
+ firstName = firstName != null ? firstName.trim() : "";
+ lastName = lastName != null ? lastName.trim() : "";
+
+ if (firstName.isEmpty()) {
+ return lastName;
+ }
+ if (lastName.isEmpty()) {
+ return firstName;
+ }
+ return firstName + " " + lastName;
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserRepository.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserRepository.java
new file mode 100644
index 000000000..690a1fcdc
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app195/user/UserRepository.java
@@ -0,0 +1,27 @@
+package test.org.springdoc.api.v31.app195.user;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.annotation.PostConstruct;
+
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class UserRepository {
+
+ private final List users = new ArrayList<>();
+
+ public List findAll() {
+ return users;
+ }
+
+ public User findById(Integer id) {
+ return users.stream().filter(u -> u.id().equals(id)).findFirst().orElse(null);
+ }
+
+ @PostConstruct
+ private void init() {
+ users.add(new User(1,"Dan Vega","danvega@gmail.com"));
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/SpringDocApp196Test.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/SpringDocApp196Test.java
new file mode 100644
index 000000000..9ec2ba77e
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/SpringDocApp196Test.java
@@ -0,0 +1,39 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2025 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.app196;
+
+import test.org.springdoc.api.v31.AbstractSpringDocTest;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+
+public class SpringDocApp196Test extends AbstractSpringDocTest {
+
+ @SpringBootApplication
+ @ComponentScan(basePackages = { "org.springdoc", "test.org.springdoc.api.v31.app196" })
+ static class SpringDocTestApp {}
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/config/ApiVersionParser.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/config/ApiVersionParser.java
new file mode 100644
index 000000000..6a4fddce9
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/config/ApiVersionParser.java
@@ -0,0 +1,24 @@
+package test.org.springdoc.api.v31.app196.config;
+
+public class ApiVersionParser implements org.springframework.web.accept.ApiVersionParser {
+
+ // allows us to use /api/v2/users instead of /api/2.0/users
+ @Override
+ public Comparable parseVersion(String version) {
+ // Remove "v" prefix if it exists (v1 becomes 1, v2 becomes 2)
+ if (version.startsWith("v") || version.startsWith("V")) {
+ version = version.substring(1);
+ }
+
+ if("api-docs".equals(version) || "index.html".equals(version)
+ || "swagger-ui-bundle.js".equals(version)
+ || "swagger-ui.css".equals(version)
+ || "index.css".equals(version)
+ || "swagger-ui-standalone-preset.js".equals(version)
+ || "favicon-32x32.png".equals(version)
+ || "favicon-16x16.png".equals(version)
+ || "swagger-initializer.js".equals(version))
+ return null;
+ return version;
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/config/WebConfig.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/config/WebConfig.java
new file mode 100644
index 000000000..fd5d199d3
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/config/WebConfig.java
@@ -0,0 +1,20 @@
+package test.org.springdoc.api.v31.app196.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.reactive.config.ApiVersionConfigurer;
+import org.springframework.web.reactive.config.WebFluxConfigurer;
+
+@Configuration
+public class WebConfig implements WebFluxConfigurer {
+
+ @Override
+ public void configureApiVersioning(ApiVersionConfigurer configurer) {
+ configurer
+ .setVersionRequired(false)
+ .addSupportedVersions("1.0","v2")
+ .setDefaultVersion("1.0")
+ .useQueryParam("version")
+ .setVersionParser(new ApiVersionParser());
+ }
+
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/User.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/User.java
new file mode 100644
index 000000000..4eff6d48d
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/User.java
@@ -0,0 +1,10 @@
+package test.org.springdoc.api.v31.app196.user;
+
+public record User(
+ Integer id,
+ String name,
+ String email
+
+ // a lot more fields here
+) {
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserController.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserController.java
new file mode 100644
index 000000000..59a5dea94
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserController.java
@@ -0,0 +1,46 @@
+package test.org.springdoc.api.v31.app196.user;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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 UserController {
+
+ private static final Logger log = LoggerFactory.getLogger(UserController.class);
+ private final UserRepository userRepository;
+ private final UserMapper userMapper;
+
+ public UserController(UserRepository userRepository, UserMapper userMapper) {
+ this.userRepository = userRepository;
+ this.userMapper = userMapper;
+ }
+
+ // USING REQUEST PARAMETER (Query Parameter) ===================================
+
+ @GetMapping(value = "/users/list", params = "version=1.0")
+ public List listUsersV1() {
+ log.info("Find All Users using request header: {}", "v1");
+ return userRepository.findAll()
+ .stream()
+ .map(userMapper::toV1)
+ .collect(Collectors.toList());
+ }
+
+ @GetMapping(value = "/users/list", params = "version=v2")
+ public List listUsersV2() {
+ log.info("Find All Users using request header: {}", "v2");
+ return userRepository.findAll()
+ .stream()
+ .map(userMapper::toV2)
+ .collect(Collectors.toList());
+ }
+
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserDTOv1.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserDTOv1.java
new file mode 100644
index 000000000..0159d09da
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserDTOv1.java
@@ -0,0 +1,4 @@
+package test.org.springdoc.api.v31.app196.user;
+
+public record UserDTOv1(Integer id, String name, String email) {
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserDTOv2.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserDTOv2.java
new file mode 100644
index 000000000..1413f0f35
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserDTOv2.java
@@ -0,0 +1,4 @@
+package test.org.springdoc.api.v31.app196.user;
+
+public record UserDTOv2(Integer id, String firstName,String lastName, String email) {
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserMapper.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserMapper.java
new file mode 100644
index 000000000..ba64a768d
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserMapper.java
@@ -0,0 +1,83 @@
+package test.org.springdoc.api.v31.app196.user;
+
+import org.springframework.stereotype.Component;
+
+/*
+ * you could also use a library like MapStruct
+ */
+@Component
+public class UserMapper {
+
+ // User to DTOs
+ public UserDTOv1 toV1(User user) {
+ return new UserDTOv1(
+ user.id(),
+ user.name(),
+ user.email()
+ );
+ }
+
+ public UserDTOv2 toV2(User user) {
+ String[] nameParts = splitName(user.name());
+ return new UserDTOv2(
+ user.id(),
+ nameParts[0], // firstName
+ nameParts[1], // lastName
+ user.email()
+ );
+ }
+
+ // DTOs to User
+ public User fromV1(UserDTOv1 dto) {
+ return new User(
+ dto.id(),
+ dto.name(),
+ dto.email()
+ );
+ }
+
+ public User fromV2(UserDTOv2 dto) {
+ String combinedName = combineName(dto.firstName(), dto.lastName());
+ return new User(
+ dto.id(),
+ combinedName,
+ dto.email()
+ );
+ }
+
+ // Helper methods
+ private String[] splitName(String fullName) {
+ if (fullName == null || fullName.trim().isEmpty()) {
+ return new String[]{"", ""};
+ }
+
+ String trimmed = fullName.trim();
+ int lastSpaceIndex = trimmed.lastIndexOf(' ');
+
+ if (lastSpaceIndex == -1) {
+ // Single word name - put it as firstName
+ return new String[]{trimmed, ""};
+ }
+
+ // Split at last space (handles middle names better)
+ // "John Smith Jr" -> "John Smith" and "Jr"
+ // "Mary Ann Smith" -> "Mary Ann" and "Smith"
+ return new String[]{
+ trimmed.substring(0, lastSpaceIndex),
+ trimmed.substring(lastSpaceIndex + 1)
+ };
+ }
+
+ private String combineName(String firstName, String lastName) {
+ firstName = firstName != null ? firstName.trim() : "";
+ lastName = lastName != null ? lastName.trim() : "";
+
+ if (firstName.isEmpty()) {
+ return lastName;
+ }
+ if (lastName.isEmpty()) {
+ return firstName;
+ }
+ return firstName + " " + lastName;
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserRepository.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserRepository.java
new file mode 100644
index 000000000..ded9db774
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app196/user/UserRepository.java
@@ -0,0 +1,27 @@
+package test.org.springdoc.api.v31.app196.user;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.annotation.PostConstruct;
+
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class UserRepository {
+
+ private final List users = new ArrayList<>();
+
+ public List findAll() {
+ return users;
+ }
+
+ public User findById(Integer id) {
+ return users.stream().filter(u -> u.id().equals(id)).findFirst().orElse(null);
+ }
+
+ @PostConstruct
+ private void init() {
+ users.add(new User(1,"Dan Vega","danvega@gmail.com"));
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app197/SpringDocApp197Test.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app197/SpringDocApp197Test.java
new file mode 100644
index 000000000..cb80d75dd
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app197/SpringDocApp197Test.java
@@ -0,0 +1,113 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * Copyright 2019-2025 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.app197;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.headers.Header;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import org.junit.jupiter.api.Test;
+import org.springdoc.core.annotations.RouterOperation;
+import reactor.core.publisher.Mono;
+import test.org.springdoc.api.v31.AbstractSpringDocTest;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.http.CacheControl;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.reactive.function.server.EntityResponse;
+import org.springframework.web.reactive.function.server.HandlerFilterFunction;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.RouterFunctions;
+import org.springframework.web.reactive.function.server.ServerResponse;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class SpringDocApp197Test extends AbstractSpringDocTest {
+
+ @Test
+ void getIconResource() {
+ webTestClient.get().uri("/icons/icon.svg").exchange()
+ .expectStatus().isOk()
+ .expectHeader().cacheControl(CacheControl.maxAge(Duration.ofHours(24)))
+ .expectBody().consumeWith(response -> {
+ byte[] body = response.getResponseBody();
+ assertNotNull(body);
+ String bodyValue = new String(body, StandardCharsets.UTF_8);
+ assertThat(bodyValue).contains("");
+ });
+ }
+
+ @Test
+ void getUnknownResource() {
+ webTestClient.get().uri("/icons/unknown.svg").exchange()
+ .expectStatus().isNotFound();
+ }
+
+ @SpringBootApplication
+ @ComponentScan(basePackages = { "org.springdoc" })
+ static class SpringDocTestApp {
+
+ @Bean
+ @RouterOperation(
+ method = RequestMethod.GET,
+ operation = @Operation(
+ operationId = "getIcon",
+ summary = "Get icons resource.",
+ responses = @ApiResponse(
+ responseCode = "200", content = @Content(mediaType = "image/svg+xml"), description = "icon resource",
+ headers = @Header(name = HttpHeaders.CACHE_CONTROL, required = true, description = "Cache-Control for icons", schema = @Schema(type = "string"))
+ )
+ )
+ )
+ public RouterFunction getIconsResourceWithCacheControl() {
+ return RouterFunctions
+ .resources("/icons/**", new ClassPathResource("icons/"))
+ .filter(HandlerFilterFunction.ofResponseProcessor(this::injectCacheControlHeader));
+ }
+
+ private Mono injectCacheControlHeader(ServerResponse serverResponse) {
+ if (serverResponse instanceof EntityResponse> entityResponse) {
+ return EntityResponse.fromObject(entityResponse.entity())
+ .header(HttpHeaders.CACHE_CONTROL, CacheControl.maxAge(24, TimeUnit.HOURS).getHeaderValue())
+ .build().cast(ServerResponse.class);
+ }
+ return Mono.just(serverResponse);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app64/WebConfiguration.java b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app64/WebConfiguration.java
new file mode 100644
index 000000000..37ace0881
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/v31/app64/WebConfiguration.java
@@ -0,0 +1,17 @@
+package test.org.springdoc.api.v31.app64;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.reactive.config.ApiVersionConfigurer;
+import org.springframework.web.reactive.config.WebFluxConfigurer;
+
+/**
+ * @author bnasslahsen
+ */
+@Configuration
+public class WebConfiguration implements WebFluxConfigurer {
+
+ public void configureApiVersioning(ApiVersionConfigurer configurer) {
+ configurer.setVersionRequired(false);
+ configurer.useQueryParam("API-Version");
+ }
+}
\ No newline at end of file
diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/icons/icon.svg b/springdoc-openapi-starter-webflux-api/src/test/resources/icons/icon.svg
new file mode 100644
index 000000000..6d210d9f2
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/resources/icons/icon.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app75.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app75.json
index c8280b78f..acc0a55dc 100644
--- a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app75.json
+++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app75.json
@@ -22,6 +22,9 @@
"required": true,
"schema": {
"type": "string",
+ "enum": [
+ "value"
+ ],
"default": "value"
}
},
diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app88.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app88.json
index 485bf016d..d84ef9dff 100644
--- a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app88.json
+++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.0.1/app88.json
@@ -22,6 +22,9 @@
"required": true,
"schema": {
"type": "string",
+ "enum": [
+ "value"
+ ],
"default": "value"
}
},
diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app193.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app193.json
new file mode 100644
index 000000000..6138f1ccf
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app193.json
@@ -0,0 +1,83 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "OpenAPI definition",
+ "version": "v0"
+ },
+ "servers": [
+ {
+ "url": "",
+ "description": "Generated server url"
+ }
+ ],
+ "paths": {
+ "/api/users/media": {
+ "get": {
+ "tags": [
+ "user-controller"
+ ],
+ "operationId": "getUsersMediaV2",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json;version=2.0": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/UserDTOv2"
+ }
+ }
+ },
+ "application/json;version=1.0": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/UserDTOv1"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "UserDTOv2": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "firstName": {
+ "type": "string"
+ },
+ "lastName": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ }
+ }
+ },
+ "UserDTOv1": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "name": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app194.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app194.json
new file mode 100644
index 000000000..523f08b83
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app194.json
@@ -0,0 +1,80 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "OpenAPI definition",
+ "version": "v0"
+ },
+ "servers": [
+ {
+ "url": "",
+ "description": "Generated server url"
+ }
+ ],
+ "paths": {
+ "/api/2.0/users": {
+ "get": {
+ "tags": [
+ "user-controller"
+ ],
+ "operationId": "findAllv2",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "*/*": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/1.0/users": {
+ "get": {
+ "tags": [
+ "user-controller"
+ ],
+ "operationId": "findAllv1",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "*/*": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "User": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "name": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app195.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app195.json
new file mode 100644
index 000000000..7170ac8b6
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app195.json
@@ -0,0 +1,99 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "OpenAPI definition",
+ "version": "v0"
+ },
+ "servers": [
+ {
+ "url": "",
+ "description": "Generated server url"
+ }
+ ],
+ "paths": {
+ "/api/users": {
+ "get": {
+ "tags": [
+ "user-controller"
+ ],
+ "operationId": "getUsersV2",
+ "parameters": [
+ {
+ "name": "X-API-Version",
+ "in": "header",
+ "schema": {
+ "type": "string",
+ "default": "1.0",
+ "enum": [
+ "2.0",
+ "1.0"
+ ]
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "*/*": {
+ "schema": {
+ "oneOf": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/UserDTOv1"
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/UserDTOv2"
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "UserDTOv2": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "firstName": {
+ "type": "string"
+ },
+ "lastName": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ }
+ }
+ },
+ "UserDTOv1": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "name": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app196.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app196.json
new file mode 100644
index 000000000..4f75b3c7e
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app196.json
@@ -0,0 +1,100 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "OpenAPI definition",
+ "version": "v0"
+ },
+ "servers": [
+ {
+ "url": "",
+ "description": "Generated server url"
+ }
+ ],
+ "paths": {
+ "/api/users/list": {
+ "get": {
+ "tags": [
+ "user-controller"
+ ],
+ "operationId": "listUsersV2",
+ "parameters": [
+ {
+ "name": "version",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "default": "1.0",
+ "enum": [
+ "v2",
+ "1.0"
+ ]
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "*/*": {
+ "schema": {
+ "oneOf": [
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/UserDTOv1"
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/UserDTOv2"
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "UserDTOv2": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "firstName": {
+ "type": "string"
+ },
+ "lastName": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ }
+ }
+ },
+ "UserDTOv1": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "name": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app197.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app197.json
new file mode 100644
index 000000000..c738d9efd
--- /dev/null
+++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app197.json
@@ -0,0 +1,40 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "OpenAPI definition",
+ "version": "v0"
+ },
+ "servers": [
+ {
+ "url": "",
+ "description": "Generated server url"
+ }
+ ],
+ "paths": {
+ "/icons/**": {
+ "get": {
+ "operationId": "getIcon",
+ "summary": "Get icons resource.",
+ "responses": {
+ "200": {
+ "description": "icon resource",
+ "content": {
+ "image/svg+xml": {}
+ },
+ "headers": {
+ "Cache-Control": {
+ "description": "Cache-Control for icons",
+ "required": true,
+ "style":"simple",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {}
+}
diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app75.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app75.json
index 48a13cd8f..9944778a3 100644
--- a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app75.json
+++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app75.json
@@ -22,7 +22,10 @@
"required": true,
"schema": {
"type": "string",
- "default": "value"
+ "default": "value",
+ "enum": [
+ "value"
+ ]
}
},
{
diff --git a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app88.json b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app88.json
index 02a8ac4be..99ef2381f 100644
--- a/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app88.json
+++ b/springdoc-openapi-starter-webflux-api/src/test/resources/results/3.1.0/app88.json
@@ -22,7 +22,10 @@
"required": true,
"schema": {
"type": "string",
- "default": "value"
+ "default": "value",
+ "enum": [
+ "value"
+ ]
}
},
{
diff --git a/springdoc-openapi-starter-webflux-scalar/pom.xml b/springdoc-openapi-starter-webflux-scalar/pom.xml
index 6760da933..c71b3ed09 100644
--- a/springdoc-openapi-starter-webflux-scalar/pom.xml
+++ b/springdoc-openapi-starter-webflux-scalar/pom.xml
@@ -3,7 +3,7 @@
org.springdoc
springdoc-openapi
- 3.0.0-RC1
+ 3.0.0
springdoc-openapi-starter-webflux-scalar
${project.artifactId}
diff --git a/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarActuatorController.java b/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarActuatorController.java
index 4f254dafe..c9e222b04 100644
--- a/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarActuatorController.java
+++ b/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarActuatorController.java
@@ -31,6 +31,7 @@
import com.scalar.maven.webjar.ScalarProperties;
import io.swagger.v3.oas.annotations.Operation;
import org.springdoc.scalar.AbstractScalarController;
+import tools.jackson.databind.ObjectMapper;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
@@ -59,9 +60,10 @@ public class ScalarActuatorController extends AbstractScalarController {
*
* @param scalarProperties the scalar properties
* @param webEndpointProperties the web endpoint properties
+ * @param objectMapper the object mapper
*/
- protected ScalarActuatorController(ScalarProperties scalarProperties, WebEndpointProperties webEndpointProperties) {
- super(scalarProperties);
+ protected ScalarActuatorController(ScalarProperties scalarProperties, WebEndpointProperties webEndpointProperties, ObjectMapper objectMapper) {
+ super(scalarProperties, objectMapper);
this.webEndpointProperties = webEndpointProperties;
}
diff --git a/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarConfiguration.java b/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarConfiguration.java
index acc15b13f..1dd9e3677 100644
--- a/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarConfiguration.java
+++ b/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarConfiguration.java
@@ -30,6 +30,7 @@
import org.springdoc.core.configuration.SpringDocConfiguration;
import org.springdoc.core.events.SpringDocAppInitializer;
import org.springdoc.core.properties.SpringDocConfigProperties;
+import tools.jackson.databind.ObjectMapper;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort;
@@ -69,14 +70,15 @@ public class ScalarConfiguration {
*
* @param scalarProperties the scalar properties
* @param springDocConfigProperties the spring doc config properties
+ * @param objectMapper the object mapper
* @return the scalar web mvc controller
*/
@Bean
@ConditionalOnProperty(name = SPRINGDOC_USE_MANAGEMENT_PORT, havingValue = "false", matchIfMissing = true)
@ConditionalOnMissingBean
@Lazy(false)
- ScalarWebFluxController scalarWebMvcController(ScalarProperties scalarProperties, SpringDocConfigProperties springDocConfigProperties) {
- return new ScalarWebFluxController(scalarProperties,springDocConfigProperties);
+ ScalarWebFluxController scalarWebMvcController(ScalarProperties scalarProperties, SpringDocConfigProperties springDocConfigProperties, ObjectMapper objectMapper) {
+ return new ScalarWebFluxController(scalarProperties,springDocConfigProperties, objectMapper);
}
/**
@@ -102,7 +104,7 @@ ForwardedHeaderTransformer forwardedHeaderTransformer() {
@ConditionalOnProperty(name = SPRINGDOC_USE_MANAGEMENT_PORT, havingValue = "false", matchIfMissing = true)
@Lazy(false)
SpringDocAppInitializer springDocScalarInitializer(ScalarProperties scalarProperties){
- return new SpringDocAppInitializer(scalarProperties.getPath(), SCALAR_ENABLED);
+ return new SpringDocAppInitializer(scalarProperties.getPath(), SCALAR_ENABLED, scalarProperties.isEnabled());
}
/**
@@ -118,13 +120,14 @@ static class SwaggerActuatorWelcomeConfiguration {
*
* @param properties the properties
* @param webEndpointProperties the web endpoint properties
+ * @param objectMapper the object mapper
* @return the scalar actuator controller
*/
@Bean
@ConditionalOnMissingBean
@Lazy(false)
- ScalarActuatorController scalarActuatorController(ScalarProperties properties, WebEndpointProperties webEndpointProperties) {
- return new ScalarActuatorController(properties,webEndpointProperties);
+ ScalarActuatorController scalarActuatorController(ScalarProperties properties, WebEndpointProperties webEndpointProperties, ObjectMapper objectMapper) {
+ return new ScalarActuatorController(properties,webEndpointProperties, objectMapper);
}
/**
@@ -135,8 +138,8 @@ ScalarActuatorController scalarActuatorController(ScalarProperties properties,
@Bean
@ConditionalOnMissingBean(name = "springDocScalarInitializer")
@Lazy(false)
- SpringDocAppInitializer springDocScalarInitializer(){
- return new SpringDocAppInitializer(DEFAULT_SCALAR_ACTUATOR_PATH, SCALAR_ENABLED);
+ SpringDocAppInitializer springDocScalarInitializer(ScalarProperties scalarProperties){
+ return new SpringDocAppInitializer(DEFAULT_SCALAR_ACTUATOR_PATH, SCALAR_ENABLED, scalarProperties.isEnabled());
}
}
diff --git a/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarWebFluxController.java b/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarWebFluxController.java
index 85597fb82..64656179b 100644
--- a/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarWebFluxController.java
+++ b/springdoc-openapi-starter-webflux-scalar/src/main/java/org/springdoc/webflux/scalar/ScalarWebFluxController.java
@@ -31,6 +31,7 @@
import com.scalar.maven.webjar.ScalarProperties;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.scalar.AbstractScalarController;
+import tools.jackson.databind.ObjectMapper;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpRequest;
@@ -61,9 +62,10 @@ public class ScalarWebFluxController extends AbstractScalarController {
*
* @param scalarProperties the scalar properties
* @param springDocConfigProperties the spring doc config properties
+ * @param objectMapper the object mapper
*/
- protected ScalarWebFluxController(ScalarProperties scalarProperties, SpringDocConfigProperties springDocConfigProperties) {
- super(scalarProperties);
+ protected ScalarWebFluxController(ScalarProperties scalarProperties, SpringDocConfigProperties springDocConfigProperties, ObjectMapper objectMapper) {
+ super(scalarProperties, objectMapper);
this.springDocConfigProperties = springDocConfigProperties;
}
diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/java/test/org/springdoc/webflux/scalar/AbstractCommonTest.java b/springdoc-openapi-starter-webflux-scalar/src/test/java/test/org/springdoc/webflux/scalar/AbstractCommonTest.java
index 78f050427..8b7c6a14b 100644
--- a/springdoc-openapi-starter-webflux-scalar/src/test/java/test/org/springdoc/webflux/scalar/AbstractCommonTest.java
+++ b/springdoc-openapi-starter-webflux-scalar/src/test/java/test/org/springdoc/webflux/scalar/AbstractCommonTest.java
@@ -12,7 +12,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.webtestclient.AutoConfigureWebTestClient;
+import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
diff --git a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app1 b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app1
index 19e300d37..e1509f923 100644
--- a/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app1
+++ b/springdoc-openapi-starter-webflux-scalar/src/test/resources/results/app1
@@ -4,8 +4,8 @@
Scalar API Reference
+ content="width=device-width, initial-scale=1"
+ name="viewport" />
@@ -16,10 +16,7 @@