Skip to content

Commit e067ee0

Browse files
committed
By Kuntal : "fix(validation): apply Meta annotations correctly in ModelResolver (#4886)"
1 parent 5761d47 commit e067ee0

File tree

2 files changed

+114
-0
lines changed

2 files changed

+114
-0
lines changed

modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1754,6 +1754,8 @@ protected boolean applyBeanValidatorAnnotations(Schema property, Annotation[] an
17541754
return modified;
17551755
}
17561756
}
1757+
// expand composed constraint meta-annotations (e.g., @Min/@Max on custom annotations)
1758+
annotations = expandValidationMetaAnnotations(annotations);
17571759
Map<String, Annotation> annos = new HashMap<>();
17581760
if (annotations != null) {
17591761
for (Annotation anno : annotations) {
@@ -1946,6 +1948,8 @@ protected boolean checkGroupValidation(Class[] groups, Set<Class> invocationGrou
19461948
}
19471949

19481950
protected boolean applyBeanValidatorAnnotationsNoGroups(Schema property, Annotation[] annotations, Schema parent, boolean applyNotNullAnnotations) {
1951+
// expand composed constraint meta-annotations (e.g., @Min/@Max on custom annotations)
1952+
annotations = expandValidationMetaAnnotations(annotations);
19491953
Map<String, Annotation> annos = new HashMap<>();
19501954
boolean modified = false;
19511955
if (annotations != null) {
@@ -2049,6 +2053,42 @@ protected boolean applyBeanValidatorAnnotationsNoGroups(Schema property, Annotat
20492053
return modified;
20502054
}
20512055

2056+
/**
2057+
* Expands provided annotations to include bean-validation constraint annotations present as meta-annotations
2058+
* on custom annotations (i.e., composed constraints like a custom @ValidStoreId annotated with @Min/@Max).
2059+
* Only javax.validation.constraints annotations are added to avoid unrelated meta-annotations.
2060+
*/
2061+
private Annotation[] expandValidationMetaAnnotations(Annotation[] annotations) {
2062+
if (annotations == null || annotations.length == 0) {
2063+
return annotations;
2064+
}
2065+
Map<String, Annotation> merged = new LinkedHashMap<>();
2066+
for (Annotation a : annotations) {
2067+
if (a != null) {
2068+
merged.put(a.annotationType().getName(), a);
2069+
}
2070+
}
2071+
try {
2072+
for (Annotation a : annotations) {
2073+
if (a == null) continue;
2074+
Annotation[] metas = a.annotationType().getAnnotations();
2075+
if (metas == null) continue;
2076+
for (Annotation meta : metas) {
2077+
if (meta == null) continue;
2078+
String name = meta.annotationType().getName();
2079+
// include only standard bean validation constraint annotations
2080+
if (name != null && name.startsWith("javax.validation.constraints")) {
2081+
merged.putIfAbsent(name, meta);
2082+
}
2083+
}
2084+
}
2085+
} catch (Throwable t) {
2086+
// be conservative: if anything goes wrong, fall back to original annotations
2087+
return annotations;
2088+
}
2089+
return merged.values().toArray(new Annotation[0]);
2090+
}
2091+
20522092
private boolean resolveSubtypes(Schema model, BeanDescription bean, ModelConverterContext context, JsonView jsonViewAnnotation) {
20532093
final List<NamedType> types = _intr().findSubtypes(bean.getClassInfo());
20542094
if (types == null) {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.swagger.v3.core.resolving;
2+
3+
import io.swagger.v3.core.converter.ModelConverters;
4+
import io.swagger.v3.oas.models.media.IntegerSchema;
5+
import io.swagger.v3.oas.models.media.Schema;
6+
import org.testng.annotations.Test;
7+
8+
import javax.validation.Constraint;
9+
import javax.validation.Payload;
10+
import javax.validation.constraints.Max;
11+
import javax.validation.constraints.Min;
12+
import javax.validation.constraints.NotNull;
13+
import java.lang.annotation.ElementType;
14+
import java.lang.annotation.Retention;
15+
import java.lang.annotation.RetentionPolicy;
16+
import java.lang.annotation.Target;
17+
import java.util.Map;
18+
19+
import static org.testng.Assert.assertEquals;
20+
import static org.testng.Assert.assertNotNull;
21+
22+
public class ComposedConstraintMetaAnnotationTest {
23+
24+
@Min(0)
25+
@Max(999)
26+
@Target({ElementType.FIELD, ElementType.PARAMETER})
27+
@Retention(RetentionPolicy.RUNTIME)
28+
@Constraint(validatedBy = {})
29+
public @interface ValidStoreId {
30+
String message() default "Invalid store ID";
31+
Class<?>[] groups() default {};
32+
Class<? extends Payload>[] payload() default {};
33+
}
34+
35+
static class TestStoreDto {
36+
@Min(0)
37+
@Max(999)
38+
@NotNull
39+
private Short storeId;
40+
41+
@ValidStoreId
42+
@NotNull
43+
private Short metaStoreId;
44+
45+
public Short getStoreId() {
46+
return storeId;
47+
}
48+
49+
public void setStoreId(Short storeId) {
50+
this.storeId = storeId;
51+
}
52+
53+
public Short getMetaStoreId() {
54+
return metaStoreId;
55+
}
56+
57+
public void setMetaStoreId(Short metaStoreId) {
58+
this.metaStoreId = metaStoreId;
59+
}
60+
}
61+
62+
@Test
63+
public void readsComposedConstraintOnDtoField() {
64+
Map<String, Schema> schemas = ModelConverters.getInstance().readAll(TestStoreDto.class);
65+
Schema model = schemas.get("TestStoreDto");
66+
assertNotNull(model, "Model should be resolved");
67+
Schema meta = (Schema) model.getProperties().get("metaStoreId");
68+
assertNotNull(meta, "metaStoreId property should exist");
69+
// Should carry over min/max from composed constraint
70+
assertEquals(((IntegerSchema) meta).getMinimum().intValue(), 0);
71+
assertEquals(((IntegerSchema) meta).getMaximum().intValue(), 999);
72+
}
73+
}
74+

0 commit comments

Comments
 (0)