diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java index 2e5ea01248..e5ab451d3d 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java @@ -56,7 +56,6 @@ import io.swagger.v3.oas.models.media.IntegerSchema; import io.swagger.v3.oas.models.media.JsonSchema; import io.swagger.v3.oas.models.media.MapSchema; -import io.swagger.v3.oas.models.media.NumberSchema; import io.swagger.v3.oas.models.media.ObjectSchema; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.StringSchema; @@ -97,6 +96,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -1566,15 +1566,14 @@ protected void applyBeanValidatorAnnotations(BeanPropertyDefinition propDef, Sch } } - protected void applyBeanValidatorAnnotations(Schema property, Annotation[] annotations, Schema parent, boolean applyNotNullAnnotations) { + protected void applyBeanValidatorAnnotations(Schema property, Annotation[] directAnnotations, Schema parent, boolean applyNotNullAnnotations) { + Collection annotations = collectTransitiveAnnotations(directAnnotations); // Allows using composite constraints Map annos = new HashMap<>(); - if (annotations != null) { - for (Annotation anno : annotations) { - annos.put(anno.annotationType().getName(), anno); - } + for (Annotation anno : annotations) { + annos.put(anno.annotationType().getName(), anno); } - if (parent != null && annotations != null && applyNotNullAnnotations) { - boolean requiredItem = Arrays.stream(annotations).anyMatch(annotation -> + if (parent != null && applyNotNullAnnotations) { + boolean requiredItem = annotations.stream().anyMatch(annotation -> NOT_NULL_ANNOTATIONS.contains(annotation.annotationType().getSimpleName()) ); if (requiredItem) { @@ -1633,6 +1632,24 @@ protected void applyBeanValidatorAnnotations(Schema property, Annotation[] annot } } + private Collection collectTransitiveAnnotations(Annotation[] annotations) { + if (annotations == null) { + return new HashSet<>(); + } + LinkedHashSet annotationsToVisit = new LinkedHashSet<>(Arrays.asList(annotations)); + Set collectedAnnotations = new HashSet<>(); + while (!annotationsToVisit.isEmpty()) { + Annotation annotation = annotationsToVisit.iterator().next(); + annotationsToVisit.remove(annotation); + if (!collectedAnnotations.contains(annotation)) { + collectedAnnotations.add(annotation); + Annotation[] annotationsOfAnnotation = annotation.annotationType().getAnnotations(); + annotationsToVisit.addAll(Arrays.asList(annotationsOfAnnotation)); + } + } + return collectedAnnotations; + } + private boolean resolveSubtypes(Schema model, BeanDescription bean, ModelConverterContext context, JsonView jsonViewAnnotation) { final List types = _intr.findSubtypes(bean.getClassInfo()); if (types == null) { diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket4799Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket4799Test.java new file mode 100644 index 0000000000..109e9ef7c4 --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket4799Test.java @@ -0,0 +1,60 @@ +package io.swagger.v3.core.resolving; + +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverterContextImpl; +import io.swagger.v3.core.jackson.ModelResolver; +import io.swagger.v3.core.resolving.resources.JsonViewObject; +import io.swagger.v3.oas.models.media.Schema; +import org.junit.Test; +import org.testng.Assert; + +import javax.validation.Constraint; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collections; +import java.util.Map; + +import static io.swagger.v3.core.resolving.SwaggerTestBase.mapper; + +public class Ticket4799Test { + + @Test + @JsonView(JsonViewObject.View.Protected.class) + public void testCompositeConstraintsAreRespected() { + ObjectMapper mapper = mapper(); + final ModelResolver modelResolver = new ModelResolver(mapper); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + Schema model = context.resolve(new AnnotatedType(ClassWithAnnotation.class)); + + Map> properties = model.getProperties(); + Assert.assertEquals(properties.size(), 1); + Schema nameSchema = properties.get("name"); + Assert.assertNotNull(nameSchema); + Assert.assertEquals(nameSchema.getMinLength(), 5); + Assert.assertEquals(nameSchema.getMaxLength(), 10); + Assert.assertEquals(nameSchema.getPattern(), "^[0-9]*$"); + Assert.assertEquals(model.getRequired(), Collections.singletonList("name")); + } + + public static final class ClassWithAnnotation { + @Pattern(regexp = "^[0-9]*$") + @CompositeAnnotation + public String name; + } + + @Size(min = 5, max = 10) + @NotNull + @Constraint(validatedBy = {}) + @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE }) + @Retention(RetentionPolicy.RUNTIME) + public @interface CompositeAnnotation { + } +}