Skip to content

Commit 91852e9

Browse files
fix: exclude overridable annotation values when parsing composed annotations
1 parent 7d89d07 commit 91852e9

2 files changed

Lines changed: 102 additions & 43 deletions

File tree

modules/swagger-core/src/main/java/io/swagger/v3/core/util/ValidationAnnotationsUtils.java

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,33 @@
22

33
import io.swagger.v3.oas.models.media.Schema;
44

5+
import javax.validation.OverridesAttribute;
56
import javax.validation.constraints.*;
67
import java.lang.annotation.Annotation;
8+
import java.lang.reflect.Method;
79
import java.math.BigDecimal;
8-
import java.util.ArrayDeque;
9-
import java.util.HashSet;
10-
import java.util.LinkedHashMap;
11-
import java.util.Map;
12-
import java.util.Queue;
13-
import java.util.Set;
10+
import java.util.*;
11+
import java.util.stream.Collectors;
1412

1513
import static io.swagger.v3.core.util.SchemaTypeUtils.*;
1614

1715
public class ValidationAnnotationsUtils {
1816

19-
public static final String JAVAX_NOT_NULL = "javax.validation.constraints.NotNull";
20-
public static final String JAVAX_NOT_EMPTY = "javax.validation.constraints.NotEmpty";
21-
public static final String JAVAX_NOT_BLANK = "javax.validation.constraints.NotBlank";
22-
public static final String JAVAX_MIN = "javax.validation.constraints.Min";
23-
public static final String JAVAX_MAX = "javax.validation.constraints.Max";
24-
public static final String JAVAX_SIZE = "javax.validation.constraints.Size";
25-
public static final String JAVAX_DECIMAL_MIN = "javax.validation.constraints.DecimalMin";
26-
public static final String JAVAX_DECIMAL_MAX = "javax.validation.constraints.DecimalMax";
27-
public static final String JAVAX_PATTERN = "javax.validation.constraints.Pattern";
28-
public static final String JAVAX_EMAIL = "javax.validation.constraints.Email";
29-
public static final String JAVAX_POSITIVE = "javax.validation.constraints.Positive";
30-
public static final String JAVAX_POSITIVE_OR_ZERO = "javax.validation.constraints.PositiveOrZero";
31-
public static final String JAVAX_NEGATIVE = "javax.validation.constraints.Negative";
32-
public static final String JAVAX_NEGATIVE_OR_ZERO = "javax.validation.constraints.NegativeOrZero";
17+
private static final String JAVA_PACKAGE_BASE = "javax.validation.constraints";
18+
public static final String JAVAX_NOT_NULL = JAVA_PACKAGE_BASE + ".NotNull";
19+
public static final String JAVAX_NOT_EMPTY = JAVA_PACKAGE_BASE + ".NotEmpty";
20+
public static final String JAVAX_NOT_BLANK = JAVA_PACKAGE_BASE + ".NotBlank";
21+
public static final String JAVAX_MIN = JAVA_PACKAGE_BASE + ".Min";
22+
public static final String JAVAX_MAX = JAVA_PACKAGE_BASE + ".Max";
23+
public static final String JAVAX_SIZE = JAVA_PACKAGE_BASE + ".Size";
24+
public static final String JAVAX_DECIMAL_MIN = JAVA_PACKAGE_BASE + ".DecimalMin";
25+
public static final String JAVAX_DECIMAL_MAX = JAVA_PACKAGE_BASE + ".DecimalMax";
26+
public static final String JAVAX_PATTERN = JAVA_PACKAGE_BASE + ".Pattern";
27+
public static final String JAVAX_EMAIL = JAVA_PACKAGE_BASE + ".Email";
28+
public static final String JAVAX_POSITIVE = JAVA_PACKAGE_BASE + ".Positive";
29+
public static final String JAVAX_POSITIVE_OR_ZERO = JAVA_PACKAGE_BASE + ".PositiveOrZero";
30+
public static final String JAVAX_NEGATIVE = JAVA_PACKAGE_BASE + ".Negative";
31+
public static final String JAVAX_NEGATIVE_OR_ZERO = JAVA_PACKAGE_BASE + ".NegativeOrZero";
3332

3433
private static final String SCHEMA_EMAIL_FORMAT_NAME = "email";
3534

@@ -249,13 +248,16 @@ public static Annotation[] expandValidationMetaAnnotations(Annotation[] annotati
249248
if (a != null) queue.add(a);
250249
}
251250
while (!queue.isEmpty()) {
252-
Annotation a = queue.poll();
253-
if (!visited.add(a.annotationType())) continue;
254-
for (Annotation meta : a.annotationType().getAnnotations()) {
251+
Annotation annotation = queue.poll();
252+
if (!visited.add(annotation.annotationType())) continue;
253+
List<Class<? extends Annotation>> annotationsThatRelyOnOverride = findOverrides(annotation);
254+
for (Annotation meta : annotation.annotationType().getAnnotations()) {
255255
if (meta == null) continue;
256256
String name = meta.annotationType().getName();
257-
if (name.startsWith("javax.validation.constraints")) {
258-
merged.putIfAbsent(name, meta);
257+
if (name.startsWith(JAVA_PACKAGE_BASE)) {
258+
if (!annotationsThatRelyOnOverride.contains(meta.annotationType())) {
259+
merged.putIfAbsent(name, meta);
260+
}
259261
} else {
260262
queue.add(meta);
261263
}
@@ -278,4 +280,25 @@ public static boolean applyNegativeOrZeroConstraint(Schema schema) {
278280
return false;
279281
}
280282

283+
/**
284+
*
285+
* @param annotation the composed constraint annotation
286+
* @return the composing annotations that are overridden with {@link OverridesAttribute}
287+
*/
288+
private static List<Class<? extends Annotation>> findOverrides(Annotation annotation) {
289+
List<Class<? extends Annotation>> overriddenConstraintAnnotations = new ArrayList<>();
290+
291+
Class<? extends Annotation> type = annotation.annotationType();
292+
293+
for (Method method : type.getDeclaredMethods()) {
294+
OverridesAttribute oa = method.getAnnotation(OverridesAttribute.class);
295+
296+
if (oa != null) {
297+
overriddenConstraintAnnotations.add(oa.constraint());
298+
}
299+
}
300+
301+
return overriddenConstraintAnnotations;
302+
}
303+
281304
}

modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/ComposedConstraintMetaAnnotationTest.java

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
import java.lang.annotation.Target;
2020
import java.util.Map;
2121

22-
import static org.testng.Assert.assertEquals;
23-
import static org.testng.Assert.assertNotNull;
22+
import static org.testng.Assert.*;
2423

2524
public class ComposedConstraintMetaAnnotationTest {
2625

@@ -56,11 +55,11 @@ public class ComposedConstraintMetaAnnotationTest {
5655
}
5756

5857
/**
59-
* Mimics how Hibernate Validator's @Range works: meta-annotations @Min/@Max carry default
58+
* Mimics how Hibernate Validator's {@code @Range} works: meta-annotations @Min/@Max carry default
6059
* values, while the actual per-use values are meant to be applied via @OverridesAttribute.
6160
* Our implementation reads meta-annotations from the annotation *type definition*, so it
6261
* always sees the defaults (min=0, max=Long.MAX_VALUE) — not whatever the caller passes
63-
* as @ValidRange(min=5, max=50). This is a known limitation documented by the test below.
62+
* as {@code @ValidRange(min=5, max=50)}. This is a known limitation documented by the test below.
6463
*/
6564
@Min(0)
6665
@Max(Long.MAX_VALUE)
@@ -79,6 +78,20 @@ public class ComposedConstraintMetaAnnotationTest {
7978
Class<? extends Payload>[] payload() default {};
8079
}
8180

81+
@Min(4)
82+
@Max(Long.MAX_VALUE)
83+
@Target({ElementType.FIELD, ElementType.PARAMETER})
84+
@Retention(RetentionPolicy.RUNTIME)
85+
@Constraint(validatedBy = {})
86+
public @interface FourOrMore {
87+
@OverridesAttribute(constraint = Max.class, name = "value")
88+
long max() default Long.MAX_VALUE;
89+
90+
String message() default "Out of range";
91+
Class<?>[] groups() default {};
92+
Class<? extends Payload>[] payload() default {};
93+
}
94+
8295
@ValidStoreId
8396
@Target({ElementType.FIELD, ElementType.PARAMETER})
8497
@Retention(RetentionPolicy.RUNTIME)
@@ -105,9 +118,6 @@ static class TestStoreDto {
105118
@ValidEmail
106119
private String email;
107120

108-
@ValidRange(min = 5, max = 50)
109-
private Short rangeField;
110-
111121
@ValidStoreIdNested
112122
private Short nestedStoreId;
113123

@@ -123,14 +133,27 @@ static class TestStoreDto {
123133
public void setName(String name) { this.name = name; }
124134
public String getEmail() { return email; }
125135
public void setEmail(String email) { this.email = email; }
126-
public Short getRangeField() { return rangeField; }
127-
public void setRangeField(Short rangeField) { this.rangeField = rangeField; }
128136
public Short getNestedStoreId() { return nestedStoreId; }
129137
public void setNestedStoreId(Short nestedStoreId) { this.nestedStoreId = nestedStoreId; }
130138
public Short getPriorityStoreId() { return priorityStoreId; }
131139
public void setPriorityStoreId(Short priorityStoreId) { this.priorityStoreId = priorityStoreId; }
132140
}
133141

142+
static class ComposedAnnotationsDto {
143+
@ValidRange(min = 5, max = 50)
144+
private Short rangeField;
145+
146+
@FourOrMore(max = 10)
147+
private Short partiallyOverriddenComposedField;
148+
149+
public Short getRangeField() { return rangeField; }
150+
public void setRangeField(Short rangeField) { this.rangeField = rangeField; }
151+
public Short getPartiallyOverriddenComposedField() { return partiallyOverriddenComposedField; }
152+
public void setPartiallyOverriddenComposedField(Short partiallyOverriddenComposedField) {
153+
this.partiallyOverriddenComposedField = partiallyOverriddenComposedField;
154+
}
155+
}
156+
134157
@Test
135158
public void readsComposedMinMaxConstraintOnDtoField() {
136159
Map<String, Schema> schemas = ModelConverters.getInstance().readAll(TestStoreDto.class);
@@ -192,23 +215,36 @@ public void directAnnotationTakesPriorityOverMetaAnnotation() {
192215
}
193216

194217
/**
195-
* Documents a known limitation: for @Range-style constraints that rely on @OverridesAttribute
218+
* Documents a known limitation: for @Range-style constraints that rely on {@link OverridesAttribute}
196219
* to propagate per-use values (e.g. @ValidRange(min=5, max=50)) into their meta-annotations
197220
* (@Min/@Max), our implementation reads constraints from the annotation *type definition*
198221
* and therefore always sees the default values (min=0, max=Long.MAX_VALUE), not the
199-
* caller-supplied ones. Handling @OverridesAttribute is not yet supported.
222+
* caller-supplied ones. Handling {@link OverridesAttribute} is not yet supported, and instead the annotation and
223+
* its composing/meta annotations are ignored entirely.
200224
*/
201225
@Test
202226
public void rangeStyleConstraintUsesDefaultsNotOverriddenValues() {
203-
Map<String, Schema> schemas = ModelConverters.getInstance().readAll(TestStoreDto.class);
204-
Schema model = schemas.get("TestStoreDto");
227+
Map<String, Schema> schemas = ModelConverters.getInstance().readAll(ComposedAnnotationsDto.class);
228+
Schema model = schemas.get("ComposedAnnotationsDto");
205229
Schema range = (Schema) model.getProperties().get("rangeField");
206230
assertNotNull(range, "rangeField property should exist");
207231
// We pick up the *default* values from @Min(0) and @Max(Long.MAX_VALUE) on the type
208-
// definition of @ValidRange, NOT the caller-supplied @ValidRange(min=5, max=50).
209-
assertEquals(range.getMinimum().longValue(), 0L,
210-
"expected default @Min(0) from type definition, not overridden min=5");
211-
assertEquals(range.getMaximum().longValue(), Long.MAX_VALUE,
212-
"expected default @Max(Long.MAX_VALUE) from type definition, not overridden max=50");
232+
// definition of @ValidRange, But we then drop them since we see that they are modified with an OverridesAttribute.
233+
assertNull(range.getMinimum(),
234+
"expected null since we drop the @Min from the overridden composed @ValidRange annotation");
235+
assertNull(range.getMaximum(),
236+
"expected null since we drop the @Max from the overridden composed @ValidRange annotation");
237+
}
238+
239+
@Test
240+
public void composedStyleConstraintUsesOnlyNonOverrideableValues() {
241+
Map<String, Schema> schemas = ModelConverters.getInstance().readAll(ComposedAnnotationsDto.class);
242+
Schema model = schemas.get("ComposedAnnotationsDto");
243+
Schema range = (Schema) model.getProperties().get("partiallyOverriddenComposedField");
244+
assertNotNull(range, "partiallyOverriddenComposedField property should exist");
245+
assertEquals(range.getMinimum().longValue(), 4L,
246+
"expected 4 from type definition since it does not have an OverridesAttribute");
247+
assertNull(range.getMaximum(),
248+
"expected null since we drop the @Max from the overridden composed @FourOrMore annotation");
213249
}
214250
}

0 commit comments

Comments
 (0)