Skip to content

Commit 353ffae

Browse files
committed
Merge branch 'mc1arke-nested-parameterobject-handling'
2 parents a16c096 + 8c07306 commit 353ffae

File tree

12 files changed

+990
-19
lines changed

12 files changed

+990
-19
lines changed

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/data/DataRestRequestService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ private void addParameters(OpenAPI openAPI, RequestMethod requestMethod, MethodA
227227
MethodParameter methodParameter, ParameterInfo parameterInfo, Parameter parameter) {
228228
List<Annotation> parameterAnnotations = Arrays.asList(getParameterAnnotations(methodParameter));
229229
if (requestBuilder.isValidParameter(parameter,methodAttributes)) {
230-
requestBuilder.applyBeanValidatorAnnotations(parameter, parameterAnnotations);
230+
requestBuilder.applyBeanValidatorAnnotations(parameter, parameterAnnotations, parameterInfo.isParameterObject());
231231
operation.addParametersItem(parameter);
232232
}
233233
else if (!RequestMethod.GET.equals(requestMethod)) {

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/extractor/MethodParameterPojoExtractor.java

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import java.time.LocalTime;
3939
import java.util.ArrayList;
4040
import java.util.Arrays;
41+
import java.util.Collection;
4142
import java.util.HashSet;
4243
import java.util.List;
4344
import java.util.Map;
@@ -50,20 +51,23 @@
5051
import java.util.concurrent.atomic.AtomicInteger;
5152
import java.util.concurrent.atomic.AtomicLong;
5253
import java.util.function.Predicate;
54+
import java.util.stream.Collectors;
5355
import java.util.stream.Stream;
5456

5557
import io.swagger.v3.core.util.PrimitiveType;
5658
import io.swagger.v3.oas.annotations.Parameter;
59+
import io.swagger.v3.oas.annotations.media.Schema;
5760

5861
import org.springframework.core.GenericTypeResolver;
5962
import org.springframework.core.MethodParameter;
6063

64+
import static org.springdoc.core.service.AbstractRequestService.hasNotNullAnnotation;
6165
import static org.springdoc.core.utils.Constants.DOT;
6266

6367
/**
6468
* The type Method parameter pojo extractor.
6569
*
66-
* @author bnasslahsen
70+
* @author bnasslahsen, michael.clarke
6771
*/
6872
public class MethodParameterPojoExtractor {
6973

@@ -113,20 +117,21 @@ private MethodParameterPojoExtractor() {
113117
* @return the stream
114118
*/
115119
static Stream<MethodParameter> extractFrom(Class<?> clazz) {
116-
return extractFrom(clazz, "");
120+
return extractFrom(clazz, "", true);
117121
}
118122

119123
/**
120124
* Extract from stream.
121125
*
122126
* @param clazz the clazz
123127
* @param fieldNamePrefix the field name prefix
128+
* @param parentRequired whether the field that hold the class currently being inspected was required or optional
124129
* @return the stream
125130
*/
126-
private static Stream<MethodParameter> extractFrom(Class<?> clazz, String fieldNamePrefix) {
131+
private static Stream<MethodParameter> extractFrom(Class<?> clazz, String fieldNamePrefix, boolean parentRequired) {
127132
return allFieldsOf(clazz).stream()
128133
.filter(field -> !field.getType().equals(clazz))
129-
.flatMap(f -> fromGetterOfField(clazz, f, fieldNamePrefix))
134+
.flatMap(f -> fromGetterOfField(clazz, f, fieldNamePrefix, parentRequired))
130135
.filter(Objects::nonNull);
131136
}
132137

@@ -136,20 +141,92 @@ private static Stream<MethodParameter> extractFrom(Class<?> clazz, String fieldN
136141
* @param paramClass the param class
137142
* @param field the field
138143
* @param fieldNamePrefix the field name prefix
144+
* @param parentRequired whether the field that holds the class currently being examined was required or optional
139145
* @return the stream
140146
*/
141-
private static Stream<MethodParameter> fromGetterOfField(Class<?> paramClass, Field field, String fieldNamePrefix) {
147+
private static Stream<MethodParameter> fromGetterOfField(Class<?> paramClass, Field field, String fieldNamePrefix, boolean parentRequired) {
142148
Class<?> type = extractType(paramClass, field);
143149

144150
if (Objects.isNull(type))
145151
return Stream.empty();
146152

147153
if (isSimpleType(type))
148-
return fromSimpleClass(paramClass, field, fieldNamePrefix);
154+
return fromSimpleClass(paramClass, field, fieldNamePrefix, parentRequired);
149155
else {
150-
String prefix = fieldNamePrefix + field.getName() + DOT;
151-
return extractFrom(type, prefix);
156+
Parameter parameter = field.getAnnotation(Parameter.class);
157+
Schema schema = field.getAnnotation(Schema.class);
158+
boolean visible = resolveVisible(parameter, schema);
159+
if (!visible) {
160+
return Stream.empty();
161+
}
162+
String prefix = fieldNamePrefix + resolveName(parameter, schema).orElse(field.getName()) + DOT;
163+
boolean isNullable = isNullable(field.getDeclaredAnnotations());
164+
return extractFrom(type, prefix, parentRequired && resolveRequired(schema, parameter, isNullable));
165+
}
166+
}
167+
168+
private static Optional<String> resolveName(Parameter parameter, Schema schema) {
169+
if (parameter != null) {
170+
return resolveNameFromParameter(parameter);
171+
}
172+
if (schema != null) {
173+
return resolveNameFromSchema(schema);
174+
}
175+
return Optional.empty();
176+
}
177+
178+
private static Optional<String> resolveNameFromParameter(Parameter parameter) {
179+
if (parameter.name().isEmpty()) {
180+
return Optional.empty();
181+
}
182+
return Optional.of(parameter.name());
183+
}
184+
185+
private static Optional<String> resolveNameFromSchema(Schema schema) {
186+
if (schema.name().isEmpty()) {
187+
return Optional.empty();
188+
}
189+
return Optional.of(schema.name());
190+
}
191+
192+
private static boolean resolveVisible(Parameter parameter, Schema schema) {
193+
if (parameter != null) {
194+
return !parameter.hidden();
195+
}
196+
if (schema != null) {
197+
return !schema.hidden();
198+
}
199+
return true;
200+
}
201+
202+
private static boolean resolveRequired(Schema schema, Parameter parameter, boolean nullable) {
203+
if (parameter != null) {
204+
return resolveRequiredFromParameter(parameter, nullable);
205+
}
206+
if (schema != null) {
207+
return resolveRequiredFromSchema(schema, nullable);
152208
}
209+
return !nullable;
210+
}
211+
212+
private static boolean resolveRequiredFromParameter(Parameter parameter, boolean nullable) {
213+
if (parameter.required()) {
214+
return true;
215+
}
216+
return !nullable;
217+
}
218+
219+
private static boolean resolveRequiredFromSchema(Schema schema, boolean nullable) {
220+
if (schema.required()) {
221+
return true;
222+
}
223+
else if (schema.requiredMode() == Schema.RequiredMode.REQUIRED) {
224+
return true;
225+
}
226+
else if (schema.requiredMode() == Schema.RequiredMode.NOT_REQUIRED) {
227+
return false;
228+
}
229+
return !nullable;
153230
}
154231

155232
/**
@@ -181,19 +258,20 @@ private static Class<?> extractType(Class<?> paramClass, Field field) {
181258
* @param fieldNamePrefix the field name prefix
182259
* @return the stream
183260
*/
184-
private static Stream<MethodParameter> fromSimpleClass(Class<?> paramClass, Field field, String fieldNamePrefix) {
261+
private static Stream<MethodParameter> fromSimpleClass(Class<?> paramClass, Field field, String fieldNamePrefix, boolean isParentRequired) {
185262
Annotation[] fieldAnnotations = field.getDeclaredAnnotations();
186263
try {
187264
Parameter parameter = field.getAnnotation(Parameter.class);
188-
boolean isNotRequired = parameter == null || !parameter.required();
265+
Schema schema = field.getAnnotation(Schema.class);
266+
boolean isNullable = isNullable(fieldAnnotations);
267+
boolean isNotRequired = !(isParentRequired && resolveRequired(schema, parameter, isNullable));
189268
if (paramClass.getSuperclass() != null && paramClass.isRecord()) {
190269
return Stream.of(paramClass.getRecordComponents())
191270
.filter(d -> d.getName().equals(field.getName()))
192271
.map(RecordComponent::getAccessor)
193272
.map(method -> new MethodParameter(method, -1))
194273
.map(methodParameter -> DelegatingMethodParameter.changeContainingClass(methodParameter, paramClass))
195274
.map(param -> new DelegatingMethodParameter(param, fieldNamePrefix + field.getName(), fieldAnnotations, param.getMethodAnnotations(), true, isNotRequired));
196-
197275
}
198276
else
199277
return Stream.of(Introspector.getBeanInfo(paramClass).getPropertyDescriptors())
@@ -273,4 +351,17 @@ public static void removeSimpleTypes(Class<?>... classes) {
273351
SIMPLE_TYPES.removeAll(Arrays.asList(classes));
274352
}
275353

354+
/**
355+
* Is nullable boolean.
356+
*
357+
* @param fieldAnnotations the field annotations
358+
* @return the boolean
359+
*/
360+
private static boolean isNullable(Annotation[] fieldAnnotations) {
361+
Collection<String> annotationSimpleNames = Arrays.stream(fieldAnnotations)
362+
.map(Annotation::annotationType)
363+
.map(Class::getSimpleName)
364+
.collect(Collectors.toSet());
365+
return !hasNotNullAnnotation(annotationSimpleNames);
366+
}
276367
}

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/ParameterInfo.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.apache.commons.lang3.StringUtils;
3232
import org.slf4j.Logger;
3333
import org.slf4j.LoggerFactory;
34+
import org.springdoc.core.extractor.DelegatingMethodParameter;
3435
import org.springdoc.core.service.GenericParameterService;
3536

3637
import org.springframework.core.MethodParameter;
@@ -337,4 +338,14 @@ public ParameterId getParameterId() {
337338
public void setParameterId(ParameterId parameterId) {
338339
this.parameterId = parameterId;
339340
}
341+
342+
/**
343+
* Is parameter object boolean.
344+
*
345+
* @return the boolean
346+
*/
347+
public boolean isParameterObject() {
348+
return methodParameter instanceof DelegatingMethodParameter delegatingMethodParameter
349+
&& delegatingMethodParameter.isParameterObject();
350+
}
340351
}

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ public Operation build(HandlerMethod handlerMethod, RequestMethod requestMethod,
343343
parameter.setDescription(paramJavadocDescription);
344344
}
345345
}
346-
applyBeanValidatorAnnotations(parameter, parameterAnnotations);
346+
applyBeanValidatorAnnotations(parameter, parameterAnnotations, parameterInfo.isParameterObject());
347347
}
348348
else if (!RequestMethod.GET.equals(requestMethod) || OpenApiVersion.OPENAPI_3_1.getVersion().equals(openAPI.getOpenapi())) {
349349
if (operation.getRequestBody() != null)
@@ -609,15 +609,16 @@ public Parameter buildParam(ParameterInfo parameterInfo, Components components,
609609
/**
610610
* Apply bean validator annotations.
611611
*
612-
* @param parameter the parameter
613-
* @param annotations the annotations
612+
* @param parameter the parameter
613+
* @param annotations the annotations
614+
* @param isParameterObject the is parameter object
614615
*/
615-
public void applyBeanValidatorAnnotations(final Parameter parameter, final List<Annotation> annotations) {
616+
public void applyBeanValidatorAnnotations(final Parameter parameter, final List<Annotation> annotations, final boolean isParameterObject) {
616617
Map<String, Annotation> annos = new HashMap<>();
617618
if (annotations != null)
618619
annotations.forEach(annotation -> annos.put(annotation.annotationType().getSimpleName(), annotation));
619-
boolean annotationExists = Arrays.stream(ANNOTATIONS_FOR_REQUIRED).anyMatch(annos::containsKey);
620-
if (annotationExists)
620+
boolean annotationExists = hasNotNullAnnotation(annos.keySet());
621+
if (annotationExists && !isParameterObject)
621622
parameter.setRequired(true);
622623
Schema<?> schema = parameter.getSchema();
623624
applyValidationsToSchema(annos, schema);
@@ -643,7 +644,7 @@ public void applyBeanValidatorAnnotations(final RequestBody requestBody, final L
643644
.filter(annotation -> io.swagger.v3.oas.annotations.parameters.RequestBody.class.equals(annotation.annotationType()))
644645
.anyMatch(annotation -> ((io.swagger.v3.oas.annotations.parameters.RequestBody) annotation).required());
645646
}
646-
boolean validationExists = Arrays.stream(ANNOTATIONS_FOR_REQUIRED).anyMatch(annos::containsKey);
647+
boolean validationExists = hasNotNullAnnotation(annos.keySet());
647648

648649
if (validationExists || (!isOptional && (springRequestBodyRequired || swaggerRequestBodyRequired)))
649650
requestBody.setRequired(true);
@@ -840,5 +841,15 @@ else if (requestBody.content().length > 0)
840841
}
841842
return false;
842843
}
844+
845+
/**
846+
* Check if the parameter has any of the annotations that make it non-optional
847+
*
848+
* @param annotationSimpleNames the annotation simple class named, e.g. NotNull
849+
* @return whether any of the known NotNull annotations are present
850+
*/
851+
public static boolean hasNotNullAnnotation(Collection<String> annotationSimpleNames) {
852+
return Arrays.stream(ANNOTATIONS_FOR_REQUIRED).anyMatch(annotationSimpleNames::contains);
853+
}
843854

844855
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * *
7+
* * * * * * Copyright 2019-2024 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+
package test.org.springdoc.api.v30.app233;
27+
28+
import io.swagger.v3.oas.annotations.Parameter;
29+
import io.swagger.v3.oas.annotations.media.Schema;
30+
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
31+
import jakarta.validation.Valid;
32+
import jakarta.validation.constraints.NotNull;
33+
import org.springdoc.core.annotations.ParameterObject;
34+
35+
import org.springframework.web.bind.annotation.GetMapping;
36+
import org.springframework.web.bind.annotation.RestController;
37+
38+
@RestController
39+
public class ParameterController {
40+
41+
@GetMapping("/hidden-parent")
42+
public void nestedParameterObjectWithHiddenParentField(@ParameterObject ParameterObjectWithHiddenField parameters) {
43+
44+
}
45+
46+
public record ParameterObjectWithHiddenField(
47+
@Schema(hidden = true) NestedParameterObject schemaHiddenNestedParameterObject,
48+
@Parameter(hidden = true) NestedParameterObject parameterHiddenNestedParameterObject,
49+
NestedParameterObject visibleNestedParameterObject
50+
) {
51+
52+
}
53+
54+
public record NestedParameterObject(
55+
String parameterField) {
56+
}
57+
58+
@GetMapping("/renamed-parent")
59+
public void nestedParameterObjectWithRenamedParentField(@ParameterObject ParameterObjectWithRenamedField parameters) {
60+
61+
}
62+
63+
public record ParameterObjectWithRenamedField(
64+
@Schema(name = "schemaRenamed") NestedParameterObject schemaRenamedNestedParameterObject,
65+
@Parameter(name = "parameterRenamed") NestedParameterObject parameterRenamedNestedParameterObject,
66+
NestedParameterObject originalNameNestedParameterObject
67+
) {
68+
69+
}
70+
71+
@GetMapping("/optional-parent")
72+
public void nestedParameterObjectWithOptionalParentField(@Valid @ParameterObject MultiFieldParameterObject parameters) {
73+
74+
}
75+
76+
public record MultiFieldParameterObject(
77+
@Valid @Schema(requiredMode = RequiredMode.REQUIRED) @NotNull MultiFieldNestedParameterObject requiredNotNullParameterObject,
78+
@Valid @Schema(requiredMode = RequiredMode.REQUIRED) MultiFieldNestedParameterObject requiredNoValidationParameterObject,
79+
@Valid @Schema(requiredMode = RequiredMode.NOT_REQUIRED) @NotNull MultiFieldNestedParameterObject notRequiredNotNullParameterObject,
80+
@Valid @Schema(requiredMode = RequiredMode.NOT_REQUIRED) MultiFieldNestedParameterObject notRequiredNoValidationParameterObject,
81+
@Valid @NotNull MultiFieldNestedParameterObject noSchemaNotNullParameterObject,
82+
@Valid MultiFieldNestedParameterObject noSchemaNoValidationParameterObject) {
83+
84+
}
85+
86+
public record MultiFieldNestedParameterObject (
87+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) @NotNull String requiredNotNullField,
88+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String requiredNoValidationField,
89+
@Schema(requiredMode = RequiredMode.NOT_REQUIRED) @NotNull String notRequiredNotNullField,
90+
@Schema(requiredMode = RequiredMode.NOT_REQUIRED) String notRequiredNoValidationField,
91+
@NotNull String noSchemaNotNullField,
92+
String noSchemaNoValidationField) {
93+
}
94+
95+
}

0 commit comments

Comments
 (0)