Skip to content

Commit e7106d3

Browse files
committed
Merge pull request #3239 from Mattias-Sehlstedt/range-validation-annotation
(cherry picked from commit f2ec2ef)
1 parent a30be7b commit e7106d3

File tree

6 files changed

+265
-33
lines changed

6 files changed

+265
-33
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,11 @@ public Parameter buildParam(ParameterInfo parameterInfo, Components components,
684684
* @param isParameterObject the is parameter object
685685
* @param openapiVersion the openapi version
686686
*/
687-
public void applyBeanValidatorAnnotations(final MethodParameter methodParameter, final Parameter parameter, final List<Annotation> annotations, final boolean isParameterObject, String openapiVersion) {
687+
public void applyBeanValidatorAnnotations(final MethodParameter methodParameter,
688+
final Parameter parameter,
689+
final List<Annotation> annotations,
690+
final boolean isParameterObject,
691+
String openapiVersion) {
688692
boolean annotatedNotNull = annotations != null && SchemaUtils.annotatedNotNull(annotations);
689693
if (annotatedNotNull && !isParameterObject) {
690694
parameter.setRequired(true);

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SchemaUtils.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.lang.reflect.Field;
77
import java.lang.reflect.Method;
88
import java.math.BigDecimal;
9+
import java.util.ArrayList;
910
import java.util.Arrays;
1011
import java.util.Collection;
1112
import java.util.HashSet;
@@ -21,6 +22,8 @@
2122
import io.swagger.v3.oas.annotations.Parameter;
2223
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
2324
import io.swagger.v3.oas.models.media.Schema;
25+
import jakarta.validation.Constraint;
26+
import jakarta.validation.OverridesAttribute;
2427
import jakarta.validation.constraints.DecimalMax;
2528
import jakarta.validation.constraints.DecimalMin;
2629
import jakarta.validation.constraints.Max;
@@ -31,6 +34,7 @@
3134
import jakarta.validation.constraints.Positive;
3235
import jakarta.validation.constraints.PositiveOrZero;
3336
import jakarta.validation.constraints.Size;
37+
import org.hibernate.validator.constraints.Range;
3438
import org.springdoc.core.properties.SpringDocConfigProperties.ApiDocs.OpenApiVersion;
3539

3640
import org.springframework.lang.Nullable;
@@ -230,7 +234,7 @@ public boolean fieldRequired(Field field, io.swagger.v3.oas.annotations.media.Sc
230234
* @param openapiVersion the openapi version
231235
*/
232236
public static void applyValidationsToSchema(Schema<?> schema, List<Annotation> annotations, String openapiVersion) {
233-
annotations.forEach(anno -> {
237+
removeComposingConstraints(annotations).forEach(anno -> {
234238
String annotationName = anno.annotationType().getSimpleName();
235239
if (annotationName.equals(Positive.class.getSimpleName())) {
236240
if (OpenApiVersion.OPENAPI_3_1.getVersion().equals(openapiVersion)) {
@@ -296,6 +300,10 @@ else if (OPENAPI_STRING_TYPE.equals(type)) {
296300
if (annotationName.equals(Pattern.class.getSimpleName())) {
297301
schema.setPattern(((Pattern) anno).regexp());
298302
}
303+
if (annotationName.equals(Range.class.getSimpleName())) {
304+
schema.setMinimum(BigDecimal.valueOf(((Range) anno).min()));
305+
schema.setMaximum(BigDecimal.valueOf(((Range) anno).max()));
306+
}
299307
});
300308
if (schema!=null && annotatedNotNull(annotations)) {
301309
String specVersion = schema.getSpecVersion().name();
@@ -305,6 +313,50 @@ else if (OPENAPI_STRING_TYPE.equals(type)) {
305313
}
306314
}
307315

316+
/**
317+
* Remove the composing constraints from the annotations. This is necessary since otherwise the annotations may
318+
* default to the composing constraints' default value (dependent on the annotation ordering).
319+
* An example is {@link Range} being a composed constraint for {@link Min} and {@link Max}.
320+
* So {@link Min} and {@link Max} are removed to ensure that the constraint values are read from {@link Range}.
321+
*
322+
* @param constraintAnnotations constraint annotations
323+
* @return the annotations where known composing constraints have been removed
324+
*/
325+
private static List<Annotation> removeComposingConstraints(List<Annotation> constraintAnnotations) {
326+
Set<Class<? extends Annotation>> composingTypes = new HashSet<>();
327+
for (Annotation ann : constraintAnnotations) {
328+
Class<? extends Annotation> type = ann.annotationType();
329+
List<Class<? extends Annotation>> annotationOverrides = findOverrides(ann);
330+
for (Annotation meta : type.getAnnotations()) {
331+
if (annotationOverrides.contains(meta.annotationType())) {
332+
composingTypes.add(meta.annotationType());
333+
}
334+
}
335+
}
336+
return constraintAnnotations.stream().filter(annotation -> !composingTypes.contains(annotation.annotationType())).toList();
337+
}
338+
339+
/**
340+
*
341+
* @param annotation the composed constraint annotation
342+
* @return the composing annotations that are overridden with {@link OverridesAttribute}
343+
*/
344+
private static List<Class<? extends Annotation>> findOverrides(Annotation annotation) {
345+
List<Class<? extends Annotation>> overriddenConstraintAnnotations = new ArrayList<>();
346+
347+
Class<? extends Annotation> type = annotation.annotationType();
348+
349+
for (Method method : type.getDeclaredMethods()) {
350+
OverridesAttribute oa = method.getAnnotation(OverridesAttribute.class);
351+
352+
if (oa != null) {
353+
overriddenConstraintAnnotations.add(oa.constraint());
354+
}
355+
}
356+
357+
return overriddenConstraintAnnotations;
358+
}
359+
308360
/**
309361
* Nullable from annotations boolean.
310362
*

springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app112/PersonController.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,24 @@
2424

2525
package test.org.springdoc.api.v30.app112;
2626

27+
import java.lang.annotation.Retention;
2728
import java.util.ArrayList;
2829
import java.util.List;
2930
import java.util.Random;
3031

3132
import jakarta.validation.Valid;
32-
import jakarta.validation.constraints.NotBlank;
33-
import jakarta.validation.constraints.NotNull;
34-
import jakarta.validation.constraints.Size;
33+
import jakarta.validation.constraints.*;
3534

35+
import org.hibernate.validator.constraints.Range;
3636
import org.springframework.validation.annotation.Validated;
3737
import org.springframework.web.bind.annotation.RequestBody;
3838
import org.springframework.web.bind.annotation.RequestMapping;
3939
import org.springframework.web.bind.annotation.RequestMethod;
4040
import org.springframework.web.bind.annotation.RequestParam;
4141
import org.springframework.web.bind.annotation.RestController;
4242

43+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
44+
4345
@RestController
4446
@Validated
4547
public class PersonController {
@@ -72,4 +74,21 @@ public List<Person> findByLastName(@RequestParam(name = "lastName", required = t
7274
return hardCoded;
7375

7476
}
77+
78+
@RequestMapping(path = "/persons", method = RequestMethod.GET)
79+
public List<Person> findPersons(
80+
@RequestParam(name = "setsOfShoes") @Range(min = 1, max = 4) int setsOfShoes,
81+
@RequestParam(name = "height") @Range(max = 200) int height,
82+
@RequestParam(name = "age") @Range(min = 2) int age,
83+
@RequestParam(name = "oneToTen") @ComposedInterfaceWithStaticDefinitions int oneToTen
84+
) {
85+
return List.of();
86+
87+
}
88+
89+
@Min(1)
90+
@Max(10)
91+
@Retention(RUNTIME)
92+
public @interface ComposedInterfaceWithStaticDefinitions {
93+
}
7594
}

springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app112/PersonController.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,24 @@
2424

2525
package test.org.springdoc.api.v31.app112;
2626

27+
import java.lang.annotation.Retention;
2728
import java.util.ArrayList;
2829
import java.util.List;
2930
import java.util.Random;
3031

3132
import jakarta.validation.Valid;
32-
import jakarta.validation.constraints.NotBlank;
33-
import jakarta.validation.constraints.NotNull;
34-
import jakarta.validation.constraints.Size;
33+
import jakarta.validation.constraints.*;
3534

35+
import org.hibernate.validator.constraints.Range;
3636
import org.springframework.validation.annotation.Validated;
3737
import org.springframework.web.bind.annotation.RequestBody;
3838
import org.springframework.web.bind.annotation.RequestMapping;
3939
import org.springframework.web.bind.annotation.RequestMethod;
4040
import org.springframework.web.bind.annotation.RequestParam;
4141
import org.springframework.web.bind.annotation.RestController;
4242

43+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
44+
4345
@RestController
4446
@Validated
4547
public class PersonController {
@@ -72,4 +74,21 @@ public List<Person> findByLastName(@RequestParam(name = "lastName", required = t
7274
return hardCoded;
7375

7476
}
77+
78+
@RequestMapping(path = "/persons", method = RequestMethod.GET)
79+
public List<Person> findPersons(
80+
@RequestParam(name = "setsOfShoes") @Range(min = 1, max = 4) int setsOfShoes,
81+
@RequestParam(name = "height") @Range(max = 200) int height,
82+
@RequestParam(name = "age") @Range(min = 2) int age,
83+
@RequestParam(name = "oneToTen") @ComposedInterfaceWithStaticDefinitions int oneToTen
84+
) {
85+
return List.of();
86+
87+
}
88+
89+
@Min(1)
90+
@Max(10)
91+
@Retention(RUNTIME)
92+
public @interface ComposedInterfaceWithStaticDefinitions {
93+
}
7594
}

springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app112.json

Lines changed: 93 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@
6464
"required": true
6565
},
6666
"responses": {
67-
"415": {
68-
"description": "Unsupported Media Type",
67+
"500": {
68+
"description": "Internal Server Error",
6969
"content": {
7070
"*/*": {
7171
"schema": {
72-
"$ref": "#/components/schemas/ErrorMessage"
72+
"$ref": "#/components/schemas/Problem"
7373
}
7474
}
7575
}
@@ -84,12 +84,12 @@
8484
}
8585
}
8686
},
87-
"500": {
88-
"description": "Internal Server Error",
87+
"415": {
88+
"description": "Unsupported Media Type",
8989
"content": {
9090
"*/*": {
9191
"schema": {
92-
"$ref": "#/components/schemas/Problem"
92+
"$ref": "#/components/schemas/ErrorMessage"
9393
}
9494
}
9595
}
@@ -107,6 +107,75 @@
107107
}
108108
}
109109
},
110+
"/persons": {
111+
"get": {
112+
"tags": [
113+
"person-controller"
114+
],
115+
"operationId": "findPersons",
116+
"parameters": [
117+
{
118+
"name": "setsOfShoes",
119+
"in": "query",
120+
"required": true,
121+
"schema": {
122+
"maximum": 4,
123+
"minimum": 1,
124+
"type": "integer",
125+
"format": "int32"
126+
}
127+
},
128+
{
129+
"name": "height",
130+
"in": "query",
131+
"required": true,
132+
"schema": {
133+
"maximum": 200,
134+
"minimum": 0,
135+
"type": "integer",
136+
"format": "int32"
137+
}
138+
},
139+
{
140+
"name": "age",
141+
"in": "query",
142+
"required": true,
143+
"schema": {
144+
"maximum": 9223372036854775807,
145+
"minimum": 2,
146+
"type": "integer",
147+
"format": "int32"
148+
}
149+
},
150+
{
151+
"name": "oneToTen",
152+
"in": "query",
153+
"required": true,
154+
"schema": {
155+
"maximum": 10,
156+
"minimum": 1,
157+
"type": "integer",
158+
"format": "int32"
159+
}
160+
}
161+
],
162+
"responses": {
163+
"200": {
164+
"description": "OK",
165+
"content": {
166+
"*/*": {
167+
"schema": {
168+
"type": "array",
169+
"items": {
170+
"$ref": "#/components/schemas/Person"
171+
}
172+
}
173+
}
174+
}
175+
}
176+
}
177+
}
178+
},
110179
"/personByLastName": {
111180
"get": {
112181
"tags": [
@@ -161,12 +230,12 @@
161230
}
162231
],
163232
"responses": {
164-
"415": {
165-
"description": "Unsupported Media Type",
233+
"500": {
234+
"description": "Internal Server Error",
166235
"content": {
167236
"*/*": {
168237
"schema": {
169-
"$ref": "#/components/schemas/ErrorMessage"
238+
"$ref": "#/components/schemas/Problem"
170239
}
171240
}
172241
}
@@ -181,12 +250,12 @@
181250
}
182251
}
183252
},
184-
"500": {
185-
"description": "Internal Server Error",
253+
"415": {
254+
"description": "Unsupported Media Type",
186255
"content": {
187256
"*/*": {
188257
"schema": {
189-
"$ref": "#/components/schemas/Problem"
258+
"$ref": "#/components/schemas/ErrorMessage"
190259
}
191260
}
192261
}
@@ -210,17 +279,6 @@
210279
},
211280
"components": {
212281
"schemas": {
213-
"ErrorMessage": {
214-
"type": "object",
215-
"properties": {
216-
"errors": {
217-
"type": "array",
218-
"items": {
219-
"type": "string"
220-
}
221-
}
222-
}
223-
},
224282
"Problem": {
225283
"type": "object",
226284
"properties": {
@@ -232,6 +290,17 @@
232290
}
233291
}
234292
},
293+
"ErrorMessage": {
294+
"type": "object",
295+
"properties": {
296+
"errors": {
297+
"type": "array",
298+
"items": {
299+
"type": "string"
300+
}
301+
}
302+
}
303+
},
235304
"Person": {
236305
"required": [
237306
"lastName"
@@ -273,4 +342,4 @@
273342
}
274343
}
275344
}
276-
}
345+
}

0 commit comments

Comments
 (0)