From d633484d24a42803582128e98e59698c7d5a746b Mon Sep 17 00:00:00 2001
From: margie1a <4987kk@naver.com>
Date: Sun, 15 Feb 2026 01:27:24 +0900
Subject: [PATCH] Fix validation mapping, path schema regression, and Boot 4
NPE
Fixes #3201
Fixes #3213
Fixes #3202
---
.../configuration/SpringDocConfiguration.java | 14 ++
.../SchemaPropertyValidationConverter.java | 122 ++++++++++++++++++
.../KotlinDeprecatedPropertyCustomizer.kt | 11 +-
...otlinDeprecatedPropertyCustomizerTest.java | 69 ++++++++++
.../api/v30/app245/HelloController.java | 70 ++++++++++
.../api/v30/app245/SpringDocApp245Test.java | 42 ++++++
.../api/v30/app248/HelloController.java | 50 +++++++
.../api/v30/app248/SpringDocApp248Test.java | 43 ++++++
.../api/v31/app248/HelloController.java | 50 +++++++
.../api/v31/app248/SpringDocApp248Test.java | 43 ++++++
.../test/resources/results/3.0.1/app245.json | 87 +++++++++++++
.../test/resources/results/3.0.1/app248.json | 50 +++++++
.../test/resources/results/3.1.0/app248.json | 50 +++++++
13 files changed, 698 insertions(+), 3 deletions(-)
create mode 100644 springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/SchemaPropertyValidationConverter.java
create mode 100644 springdoc-openapi-starter-common/src/test/java/org/springdoc/core/customizers/KotlinDeprecatedPropertyCustomizerTest.java
create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app245/HelloController.java
create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app245/SpringDocApp245Test.java
create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app248/HelloController.java
create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app248/SpringDocApp248Test.java
create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/HelloController.java
create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/SpringDocApp248Test.java
create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app245.json
create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app248.json
create mode 100644 springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app248.json
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java
index 0b3154fcf..4bb7e89eb 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
@@ -58,6 +58,7 @@
import org.springdoc.core.converters.PropertyCustomizingConverter;
import org.springdoc.core.converters.ResponseSupportConverter;
import org.springdoc.core.converters.SchemaPropertyDeprecatingConverter;
+import org.springdoc.core.converters.SchemaPropertyValidationConverter;
import org.springdoc.core.converters.WebFluxSupportConverter;
import org.springdoc.core.customizers.ActuatorOperationCustomizer;
import org.springdoc.core.customizers.DataRestRouterOperationCustomizer;
@@ -275,6 +276,19 @@ SchemaPropertyDeprecatingConverter schemaPropertyDeprecatingConverter() {
return new SchemaPropertyDeprecatingConverter();
}
+ /**
+ * Schema property validation converter schema property validation converter.
+ *
+ * @param springDocConfigProperties the spring doc config properties
+ * @return the schema property validation converter
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ @Lazy(false)
+ SchemaPropertyValidationConverter schemaPropertyValidationConverter(SpringDocConfigProperties springDocConfigProperties) {
+ return new SchemaPropertyValidationConverter(springDocConfigProperties);
+ }
+
/**
* Polymorphic model converter polymorphic model converter.
*
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/SchemaPropertyValidationConverter.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/SchemaPropertyValidationConverter.java
new file mode 100644
index 000000000..bfe9eac93
--- /dev/null
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/SchemaPropertyValidationConverter.java
@@ -0,0 +1,122 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * 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.converters;
+
+import java.lang.annotation.Annotation;
+import java.math.BigDecimal;
+import java.util.Iterator;
+
+import io.swagger.v3.core.converter.AnnotatedType;
+import io.swagger.v3.core.converter.ModelConverter;
+import io.swagger.v3.core.converter.ModelConverterContext;
+import io.swagger.v3.oas.models.media.Schema;
+import jakarta.validation.constraints.Negative;
+import jakarta.validation.constraints.NegativeOrZero;
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.PositiveOrZero;
+import org.springdoc.core.properties.SpringDocConfigProperties;
+
+import static org.springdoc.core.properties.SpringDocConfigProperties.ApiDocs.OpenApiVersion;
+
+/**
+ * The type Schema property validation converter.
+ * Applies Jakarta Bean Validation annotations ({@link Positive}, {@link PositiveOrZero},
+ * {@link Negative}, {@link NegativeOrZero}) to schema properties.
+ *
+ * These annotations are not natively handled by swagger-core's ModelResolver for model
+ * properties, so this converter fills the gap.
+ *
+ * @author springdoc
+ */
+public class SchemaPropertyValidationConverter implements ModelConverter {
+
+ /**
+ * The spring doc config properties.
+ */
+ private final SpringDocConfigProperties springDocConfigProperties;
+
+ /**
+ * Instantiates a new Schema property validation converter.
+ *
+ * @param springDocConfigProperties the spring doc config properties
+ */
+ public SchemaPropertyValidationConverter(SpringDocConfigProperties springDocConfigProperties) {
+ this.springDocConfigProperties = springDocConfigProperties;
+ }
+
+ @Override
+ public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterator chain) {
+ if (chain.hasNext()) {
+ Schema> resolvedSchema = chain.next().resolve(type, context, chain);
+ if (type.isSchemaProperty() && resolvedSchema != null) {
+ applyBeanValidationAnnotations(resolvedSchema, type.getCtxAnnotations());
+ }
+ return resolvedSchema;
+ }
+ return null;
+ }
+
+ /**
+ * Apply bean validation annotations to the schema.
+ *
+ * @param schema the schema
+ * @param annotations the annotations
+ */
+ private void applyBeanValidationAnnotations(Schema> schema, Annotation[] annotations) {
+ if (annotations == null) {
+ return;
+ }
+ String openapiVersion = springDocConfigProperties.getApiDocs().getVersion().getVersion();
+ for (Annotation annotation : annotations) {
+ Class extends Annotation> annotationType = annotation.annotationType();
+ if (annotationType == Positive.class) {
+ if (OpenApiVersion.OPENAPI_3_1.getVersion().equals(openapiVersion)) {
+ schema.setExclusiveMinimumValue(BigDecimal.ZERO);
+ }
+ else {
+ schema.setMinimum(BigDecimal.ZERO);
+ schema.setExclusiveMinimum(true);
+ }
+ }
+ else if (annotationType == PositiveOrZero.class) {
+ schema.setMinimum(BigDecimal.ZERO);
+ }
+ else if (annotationType == NegativeOrZero.class) {
+ schema.setMaximum(BigDecimal.ZERO);
+ }
+ else if (annotationType == Negative.class) {
+ if (OpenApiVersion.OPENAPI_3_1.getVersion().equals(openapiVersion)) {
+ schema.setExclusiveMaximumValue(BigDecimal.ZERO);
+ }
+ else {
+ schema.setMaximum(BigDecimal.ZERO);
+ schema.setExclusiveMaximum(true);
+ }
+ }
+ }
+ }
+}
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/KotlinDeprecatedPropertyCustomizer.kt b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/KotlinDeprecatedPropertyCustomizer.kt
index 41b2dfc10..5155afdfe 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/KotlinDeprecatedPropertyCustomizer.kt
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/KotlinDeprecatedPropertyCustomizer.kt
@@ -34,7 +34,6 @@ import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.media.Schema
import org.springdoc.core.providers.ObjectMapperProvider
import kotlin.reflect.full.findAnnotation
-import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
/**
@@ -52,7 +51,14 @@ class KotlinDeprecatedPropertyCustomizer(
): Schema<*>? {
if (!chain.hasNext()) return null
// Resolve the next model in the chain
- val resolvedSchema = chain.next().resolve(type, context, chain)
+ val resolvedSchema = try {
+ chain.next().resolve(type, context, chain)
+ }
+ catch (_: NullPointerException) {
+ // Some swagger-core subtype resolution paths can throw NPE (e.g. Spring Boot 4 + oneOf usage).
+ // Keep schema generation graceful and continue without this schema.
+ return null
+ }
val javaType: JavaType =
objectMapperProvider.jsonMapper().constructType(type.type)
@@ -65,7 +71,6 @@ class KotlinDeprecatedPropertyCustomizer(
// Check each property of the class
for (prop in kotlinClass.memberProperties) {
val deprecatedAnnotation = prop.findAnnotation()
- prop.hasAnnotation()
if (deprecatedAnnotation != null) {
val fieldName = prop.name
if (resolvedSchema!=null && resolvedSchema.`$ref` != null) {
diff --git a/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/customizers/KotlinDeprecatedPropertyCustomizerTest.java b/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/customizers/KotlinDeprecatedPropertyCustomizerTest.java
new file mode 100644
index 000000000..079052148
--- /dev/null
+++ b/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/customizers/KotlinDeprecatedPropertyCustomizerTest.java
@@ -0,0 +1,69 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * 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.customizers;
+
+import java.util.Collections;
+import java.util.Iterator;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.swagger.v3.core.converter.AnnotatedType;
+import io.swagger.v3.core.converter.ModelConverter;
+import io.swagger.v3.core.converter.ModelConverterContext;
+import io.swagger.v3.oas.models.media.Schema;
+import org.junit.jupiter.api.Test;
+import org.springdoc.core.providers.ObjectMapperProvider;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link KotlinDeprecatedPropertyCustomizer}.
+ *
+ * @author springdoc
+ */
+class KotlinDeprecatedPropertyCustomizerTest {
+
+ @Test
+ void resolveShouldGracefullyHandleNullPointerFromNextConverter() {
+ ObjectMapperProvider objectMapperProvider = mock(ObjectMapperProvider.class);
+ when(objectMapperProvider.jsonMapper()).thenReturn(new ObjectMapper());
+
+ ModelConverter nextConverter = mock(ModelConverter.class);
+ when(nextConverter.resolve(any(), any(), any())).thenThrow(new NullPointerException("subtypeModel"));
+
+ KotlinDeprecatedPropertyCustomizer customizer = new KotlinDeprecatedPropertyCustomizer(objectMapperProvider);
+ ModelConverterContext context = mock(ModelConverterContext.class);
+ AnnotatedType type = new AnnotatedType().type(String.class);
+ Iterator chain = Collections.singletonList(nextConverter).iterator();
+
+ Schema> schema = assertDoesNotThrow(() -> customizer.resolve(type, context, chain));
+ assertNull(schema);
+ }
+}
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app245/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app245/HelloController.java
new file mode 100644
index 000000000..f618adbed
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app245/HelloController.java
@@ -0,0 +1,70 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * 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.v30.app245;
+
+import java.math.BigDecimal;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Negative;
+import jakarta.validation.constraints.NegativeOrZero;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.PositiveOrZero;
+import jakarta.validation.constraints.Size;
+
+import org.springframework.lang.Nullable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Test controller for validation-to-schema mapping.
+ *
+ * @author springdoc
+ */
+@RestController
+public class HelloController {
+
+ public record UnloadAmountRequest(
+ @NotNull @Positive BigDecimal amount,
+ @Nullable String currency,
+ @Nullable String description,
+ @Nullable @Size(max = 40)
+ @Pattern(regexp = "^[a-zA-Z0-9-]+$") String transactionReference,
+ @PositiveOrZero BigDecimal fee,
+ @NegativeOrZero BigDecimal discount,
+ @Negative BigDecimal adjustment
+ ) {
+ }
+
+ @PostMapping(value = "/unload")
+ public String unload(@Valid @RequestBody UnloadAmountRequest request) {
+ return "OK";
+ }
+
+}
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app245/SpringDocApp245Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app245/SpringDocApp245Test.java
new file mode 100644
index 000000000..915600e72
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app245/SpringDocApp245Test.java
@@ -0,0 +1,42 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * 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.v30.app245;
+
+import test.org.springdoc.api.v30.AbstractSpringDocV30Test;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * OpenAPI 3.0 test for Jakarta validation mapping.
+ *
+ * @author springdoc
+ */
+public class SpringDocApp245Test extends AbstractSpringDocV30Test {
+
+ @SpringBootApplication
+ static class SpringDocTestApp {}
+}
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app248/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app248/HelloController.java
new file mode 100644
index 000000000..a33a49063
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app248/HelloController.java
@@ -0,0 +1,50 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * 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.v30.app248;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Test controller for path variable schema mapping.
+ *
+ * @author springdoc
+ */
+@RestController
+@RequestMapping("/app248")
+public class HelloController {
+
+ @GetMapping("/{id}")
+ public String getById(@PathVariable @Schema(type = "integer", format = "int64", description = "Entity ID") String id) {
+ return id;
+ }
+}
+
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app248/SpringDocApp248Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app248/SpringDocApp248Test.java
new file mode 100644
index 000000000..2a7fb4cf0
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app248/SpringDocApp248Test.java
@@ -0,0 +1,43 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * 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.v30.app248;
+
+import test.org.springdoc.api.v30.AbstractSpringDocV30Test;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * OpenAPI 3.0 test for @PathVariable + @Schema handling.
+ *
+ * @author springdoc
+ */
+public class SpringDocApp248Test extends AbstractSpringDocV30Test {
+
+ @SpringBootApplication
+ static class SpringDocTestApp {}
+}
+
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/HelloController.java
new file mode 100644
index 000000000..533fc8b96
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/HelloController.java
@@ -0,0 +1,50 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * 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.app248;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Test controller for path variable schema mapping.
+ *
+ * @author springdoc
+ */
+@RestController
+@RequestMapping("/app248")
+public class HelloController {
+
+ @GetMapping("/{id}")
+ public String getById(@PathVariable @Schema(type = "integer", format = "int64", description = "Entity ID") String id) {
+ return id;
+ }
+}
+
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/SpringDocApp248Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/SpringDocApp248Test.java
new file mode 100644
index 000000000..d4a8dba24
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app248/SpringDocApp248Test.java
@@ -0,0 +1,43 @@
+/*
+ *
+ * *
+ * * *
+ * * * *
+ * * * * *
+ * * * * * * 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.app248;
+
+import test.org.springdoc.api.v31.AbstractSpringDocTest;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * OpenAPI 3.1 test for @PathVariable + @Schema handling.
+ *
+ * @author springdoc
+ */
+public class SpringDocApp248Test extends AbstractSpringDocTest {
+
+ @SpringBootApplication
+ static class SpringDocTestApp {}
+}
+
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app245.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app245.json
new file mode 100644
index 000000000..6e32f322d
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app245.json
@@ -0,0 +1,87 @@
+{
+ "openapi": "3.0.1",
+ "info": {
+ "title": "OpenAPI definition",
+ "version": "v0"
+ },
+ "servers": [
+ {
+ "url": "http://localhost",
+ "description": "Generated server url"
+ }
+ ],
+ "paths": {
+ "/unload": {
+ "post": {
+ "tags": [
+ "hello-controller"
+ ],
+ "operationId": "unload",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UnloadAmountRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "*/*": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "UnloadAmountRequest": {
+ "required": [
+ "amount"
+ ],
+ "type": "object",
+ "properties": {
+ "amount": {
+ "exclusiveMinimum": true,
+ "minimum": 0,
+ "type": "number"
+ },
+ "currency": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "transactionReference": {
+ "maxLength": 40,
+ "minLength": 0,
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9-]+$"
+ },
+ "fee": {
+ "minimum": 0,
+ "type": "number"
+ },
+ "discount": {
+ "maximum": 0,
+ "type": "number"
+ },
+ "adjustment": {
+ "exclusiveMaximum": true,
+ "maximum": 0,
+ "type": "number"
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app248.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app248.json
new file mode 100644
index 000000000..057605de9
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app248.json
@@ -0,0 +1,50 @@
+{
+ "openapi": "3.0.1",
+ "info": {
+ "title": "OpenAPI definition",
+ "version": "v0"
+ },
+ "servers": [
+ {
+ "url": "http://localhost",
+ "description": "Generated server url"
+ }
+ ],
+ "paths": {
+ "/app248/{id}": {
+ "get": {
+ "tags": [
+ "hello-controller"
+ ],
+ "operationId": "getById",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "description": "Entity ID",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "description": "Entity ID",
+ "format": "int64"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "*/*": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {}
+}
+
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app248.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app248.json
new file mode 100644
index 000000000..13df06afb
--- /dev/null
+++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app248.json
@@ -0,0 +1,50 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "OpenAPI definition",
+ "version": "v0"
+ },
+ "servers": [
+ {
+ "url": "http://localhost",
+ "description": "Generated server url"
+ }
+ ],
+ "paths": {
+ "/app248/{id}": {
+ "get": {
+ "tags": [
+ "hello-controller"
+ ],
+ "operationId": "getById",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "description": "Entity ID",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int64",
+ "description": "Entity ID"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "*/*": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {}
+}
+