Skip to content

Commit d08faeb

Browse files
committed
Support @Schema annotations on Kotlin value classes #3168
1 parent 491b853 commit d08faeb

File tree

5 files changed

+208
-3
lines changed

5 files changed

+208
-3
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * *
7+
* * * * * * Copyright 2019-2025 the original author or authors.
8+
* * * * * *
9+
* * * * * * Licensed under the Apache License, Version 2.0 (the "License");
10+
* * * * * * you may not use this file except in compliance with the License.
11+
* * * * * * You may obtain a copy of the License at
12+
* * * * * *
13+
* * * * * * https://www.apache.org/licenses/LICENSE-2.0
14+
* * * * * *
15+
* * * * * * Unless required by applicable law or agreed to in writing, software
16+
* * * * * * distributed under the License is distributed on an "AS IS" BASIS,
17+
* * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* * * * * * See the License for the specific language governing permissions and
19+
* * * * * * limitations under the License.
20+
* * * * *
21+
* * * *
22+
* * *
23+
* *
24+
*
25+
*/
26+
27+
package org.springdoc.core.converters;
28+
29+
import java.lang.annotation.Annotation;
30+
import java.lang.reflect.Method;
31+
import java.lang.reflect.Type;
32+
33+
import kotlin.jvm.JvmInline;
34+
import kotlin.reflect.KClass;
35+
import kotlin.reflect.KFunction;
36+
import kotlin.reflect.KParameter;
37+
import kotlin.reflect.jvm.ReflectJvmMapping;
38+
import kotlin.reflect.jvm.internal.KClassImpl;
39+
40+
import org.springframework.core.MethodParameter;
41+
42+
/**
43+
* @author bnasslahsen
44+
*/
45+
// KotlinInlineParameterResolver.java
46+
public final class KotlinInlineParameterResolver {
47+
48+
private KotlinInlineParameterResolver() {}
49+
50+
public static Class<?> resolveInlineType(MethodParameter methodParameter, Type type) {
51+
Method method = methodParameter.getMethod();
52+
if (method == null) return null;
53+
54+
KFunction<?> kFunction = ReflectJvmMapping.getKotlinFunction(method);
55+
if (kFunction == null) return null;
56+
57+
int paramIndex = methodParameter.getParameterIndex();
58+
59+
KParameter kParam = kFunction.getParameters().stream()
60+
.filter(p -> p.getKind() == KParameter.Kind.VALUE)
61+
.skip(paramIndex)
62+
.findFirst()
63+
.orElse(null);
64+
65+
if (kParam == null) return null;
66+
67+
Object classifier = kParam.getType().getClassifier();
68+
if (!(classifier instanceof KClass<?> kClass)) return null;
69+
70+
for (Annotation a : kClass.getAnnotations()) {
71+
if (a.annotationType() == JvmInline.class
72+
&& kClass instanceof KClassImpl<?> impl) {
73+
return impl.getJClass();
74+
}
75+
}
76+
77+
return null;
78+
}
79+
}

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericParameterService.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import java.util.Map;
4242
import java.util.Objects;
4343
import java.util.Optional;
44+
import java.util.stream.Stream;
4445

4546
import com.fasterxml.jackson.annotation.JsonView;
4647
import io.swagger.v3.core.util.AnnotationsUtils;
@@ -63,6 +64,7 @@
6364
import org.apache.commons.lang3.reflect.FieldUtils;
6465
import org.slf4j.Logger;
6566
import org.slf4j.LoggerFactory;
67+
import org.springdoc.core.converters.KotlinInlineParameterResolver;
6668
import org.springdoc.core.extractor.DelegatingMethodParameter;
6769
import org.springdoc.core.extractor.MethodParameterPojoExtractor;
6870
import org.springdoc.core.models.ParameterInfo;
@@ -79,6 +81,7 @@
7981
import org.springframework.beans.factory.config.BeanExpressionResolver;
8082
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
8183
import org.springframework.core.GenericTypeResolver;
84+
import org.springframework.core.KotlinDetector;
8285
import org.springframework.core.MethodParameter;
8386
import org.springframework.core.ResolvableType;
8487
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -391,15 +394,33 @@ Schema calculateSchema(Components components, ParameterInfo parameterInfo, Reque
391394

392395
if (parameterInfo.getParameterModel() == null || parameterInfo.getParameterModel().getSchema() == null) {
393396
Type type = GenericTypeResolver.resolveType(methodParameter.getGenericParameterType(), methodParameter.getContainingClass());
397+
Annotation[] paramAnnotations = getParameterAnnotations(methodParameter);
398+
Annotation[] typeAnnotations = new Annotation[0];
399+
if (KotlinDetector.isKotlinPresent()
400+
&& KotlinDetector.isKotlinReflectPresent()
401+
&& KotlinDetector.isKotlinType(methodParameter.getContainingClass())
402+
&& type == String.class) {
403+
Class<?> restored = KotlinInlineParameterResolver
404+
.resolveInlineType(methodParameter, type);
405+
if (restored != null) {
406+
type = restored;
407+
typeAnnotations = ((Class<?>) type).getAnnotations();
408+
}
409+
}
410+
Annotation[] mergedAnnotations =
411+
Stream.concat(
412+
Arrays.stream(paramAnnotations),
413+
Arrays.stream(typeAnnotations)
414+
).toArray(Annotation[]::new);
394415
if (type instanceof Class && !((Class<?>) type).isEnum() && optionalWebConversionServiceProvider.isPresent()) {
395416
WebConversionServiceProvider webConversionServiceProvider = optionalWebConversionServiceProvider.get();
396-
if (!MethodParameterPojoExtractor.isSwaggerPrimitiveType((Class) type) && methodParameter.getParameterType().getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class) == null) {
397-
Class<?> springConvertedType = webConversionServiceProvider.getSpringConvertedType(methodParameter.getParameterType());
417+
if (!MethodParameterPojoExtractor.isSwaggerPrimitiveType((Class) type) && Arrays.stream(mergedAnnotations)
418+
.noneMatch(a -> a.annotationType() == io.swagger.v3.oas.annotations.media.Schema.class)) { Class<?> springConvertedType = webConversionServiceProvider.getSpringConvertedType(methodParameter.getParameterType());
398419
if (!(String.class.equals(springConvertedType) && ((Class<?>) type).isEnum()) && requestBodyInfo == null)
399420
type = springConvertedType;
400421
}
401422
}
402-
schemaN = SpringDocAnnotationsUtils.extractSchema(components, type, jsonView, getParameterAnnotations(methodParameter), propertyResolverUtils.getSpecVersion());
423+
schemaN = SpringDocAnnotationsUtils.extractSchema(components, type, jsonView, mergedAnnotations, propertyResolverUtils.getSpecVersion());
403424
}
404425
else
405426
schemaN = parameterInfo.getParameterModel().getSchema();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
*
3+
* * Copyright 2019-2020 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.api.v31.app12
20+
21+
import io.swagger.v3.oas.annotations.media.Schema
22+
import org.springframework.web.bind.annotation.GetMapping
23+
import org.springframework.web.bind.annotation.PathVariable
24+
import org.springframework.web.bind.annotation.RestController
25+
26+
27+
@Schema(description = "Office ID", pattern = "^[0-9A-HJKMNP-TV-Z]{26}\$")
28+
@JvmInline
29+
value class OfficeId(private val value: String)
30+
31+
@RestController
32+
class OfficeController {
33+
@GetMapping("/v1/offices/{officeId}")
34+
suspend fun getOffice(@PathVariable officeId: OfficeId): String {
35+
return "ok";
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
*
3+
* * Copyright 2019-2020 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.api.v31.app12
20+
21+
import org.springframework.boot.autoconfigure.SpringBootApplication
22+
import org.springframework.context.annotation.ComponentScan
23+
import test.org.springdoc.api.v31.AbstractKotlinSpringDocTest
24+
25+
class SpringDocApp12Test : AbstractKotlinSpringDocTest() {
26+
27+
@SpringBootApplication
28+
@ComponentScan(basePackages = ["org.springdoc", "test.org.springdoc.api.v31.app12"])
29+
class DemoApplication
30+
31+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"openapi": "3.1.0",
3+
"info": {
4+
"title": "OpenAPI definition",
5+
"version": "v0"
6+
},
7+
"servers": [
8+
{
9+
"url": "",
10+
"description": "Generated server url"
11+
}
12+
],
13+
"paths": {
14+
"/v1/offices/{officeId}": {
15+
"get": {
16+
"tags": [
17+
"office-controller"
18+
],
19+
"operationId": "getOffice-mB_CmrA",
20+
"parameters": [],
21+
"responses": {
22+
"200": {
23+
"description": "OK",
24+
"content": {
25+
"*/*": {
26+
"schema": {
27+
"type": "string"
28+
}
29+
}
30+
}
31+
}
32+
}
33+
}
34+
}
35+
},
36+
"components": {}
37+
}

0 commit comments

Comments
 (0)