Skip to content

Commit 9268835

Browse files
kuntal1461ewaostrowska
authored andcommitted
By Kuntal : "fix(validation): apply Meta annotations correctly in ModelResolver (#4886)"
1 parent 9e2cde1 commit 9268835

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
@@ -1787,6 +1787,8 @@ protected boolean applyBeanValidatorAnnotations(Schema property, Annotation[] an
17871787
return modified;
17881788
}
17891789
}
1790+
// expand composed constraint meta-annotations (e.g., @Min/@Max on custom annotations)
1791+
annotations = expandValidationMetaAnnotations(annotations);
17901792
Map<String, Annotation> annos = new HashMap<>();
17911793
if (annotations != null) {
17921794
for (Annotation anno : annotations) {
@@ -1979,6 +1981,8 @@ protected boolean checkGroupValidation(Class[] groups, Set<Class> invocationGrou
19791981
}
19801982

19811983
protected boolean applyBeanValidatorAnnotationsNoGroups(Schema property, Annotation[] annotations, Schema parent, boolean applyNotNullAnnotations) {
1984+
// expand composed constraint meta-annotations (e.g., @Min/@Max on custom annotations)
1985+
annotations = expandValidationMetaAnnotations(annotations);
19821986
Map<String, Annotation> annos = new HashMap<>();
19831987
boolean modified = false;
19841988
if (annotations != null) {
@@ -2082,6 +2086,42 @@ protected boolean applyBeanValidatorAnnotationsNoGroups(Schema property, Annotat
20822086
return modified;
20832087
}
20842088

2089+
/**
2090+
* Expands provided annotations to include bean-validation constraint annotations present as meta-annotations
2091+
* on custom annotations (i.e., composed constraints like a custom @ValidStoreId annotated with @Min/@Max).
2092+
* Only javax.validation.constraints annotations are added to avoid unrelated meta-annotations.
2093+
*/
2094+
private Annotation[] expandValidationMetaAnnotations(Annotation[] annotations) {
2095+
if (annotations == null || annotations.length == 0) {
2096+
return annotations;
2097+
}
2098+
Map<String, Annotation> merged = new LinkedHashMap<>();
2099+
for (Annotation a : annotations) {
2100+
if (a != null) {
2101+
merged.put(a.annotationType().getName(), a);
2102+
}
2103+
}
2104+
try {
2105+
for (Annotation a : annotations) {
2106+
if (a == null) continue;
2107+
Annotation[] metas = a.annotationType().getAnnotations();
2108+
if (metas == null) continue;
2109+
for (Annotation meta : metas) {
2110+
if (meta == null) continue;
2111+
String name = meta.annotationType().getName();
2112+
// include only standard bean validation constraint annotations
2113+
if (name != null && name.startsWith("javax.validation.constraints")) {
2114+
merged.putIfAbsent(name, meta);
2115+
}
2116+
}
2117+
}
2118+
} catch (Throwable t) {
2119+
// be conservative: if anything goes wrong, fall back to original annotations
2120+
return annotations;
2121+
}
2122+
return merged.values().toArray(new Annotation[0]);
2123+
}
2124+
20852125
private boolean resolveSubtypes(Schema model, BeanDescription bean, ModelConverterContext context, JsonView jsonViewAnnotation) {
20862126
final List<NamedType> types = _intr().findSubtypes(bean.getClassInfo());
20872127
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)