Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,6 @@ private void addComponentsSchemaRef(Components components, Set<String> reference
}

protected OpenAPI removeBrokenReferenceDefinitions(OpenAPI openApi) {

if (openApi == null || openApi.getComponents() == null || openApi.getComponents().getSchemas() == null) {
return openApi;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1780,6 +1780,7 @@ protected boolean applyBeanValidatorAnnotations(Schema property, Annotation[] an
return modified;
}
}
annotations = ValidationAnnotationsUtils.expandValidationMetaAnnotations(annotations);
Map<String, Annotation> annos = new HashMap<>();
if (annotations != null) {
for (Annotation anno : annotations) {
Expand Down Expand Up @@ -1950,6 +1951,7 @@ protected boolean checkGroupValidation(Class[] groups, Set<Class> invocationGrou
}

protected boolean applyBeanValidatorAnnotationsNoGroups(Schema property, Annotation[] annotations, Schema parent, boolean applyNotNullAnnotations) {
annotations = ValidationAnnotationsUtils.expandValidationMetaAnnotations(annotations);
Map<String, Annotation> annos = new HashMap<>();
boolean modified = false;
if (annotations != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import io.swagger.v3.oas.models.media.Schema;

import javax.validation.constraints.*;
import java.lang.annotation.Annotation;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;

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

Expand Down Expand Up @@ -220,6 +223,40 @@ public static boolean applyNegativeConstraint(Schema schema) {
return false;
}

/**
* Expands annotations to include bean-validation constraint annotations present as meta-annotations
* on custom composed constraints (e.g. a custom {@code @ValidStoreId} annotated with {@code @Min}/{@code @Max}).
* Direct annotations take priority over meta-annotations (putIfAbsent).
*/
public static Annotation[] expandValidationMetaAnnotations(Annotation[] annotations) {
if (annotations == null || annotations.length == 0) {
return annotations;
}
Map<String, Annotation> merged = new LinkedHashMap<>();
for (Annotation a : annotations) {
if (a != null) {
merged.put(a.annotationType().getName(), a);
}
}
try {
for (Annotation a : annotations) {
if (a == null) continue;
Annotation[] metas = a.annotationType().getAnnotations();
if (metas == null) continue;
for (Annotation meta : metas) {
if (meta == null) continue;
String name = meta.annotationType().getName();
if (name != null && name.startsWith("javax.validation.constraints")) {
merged.putIfAbsent(name, meta);
}
}
}
} catch (Throwable t) {
return annotations;
}
return merged.values().toArray(new Annotation[0]);
}

public static boolean applyNegativeOrZeroConstraint(Schema schema) {
if (isNumberSchema(schema)) {
BigDecimal current = schema.getMaximum();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package io.swagger.v3.core.resolving;

import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.Schema;
import org.testng.annotations.Test;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
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.Map;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;

public class ComposedConstraintMetaAnnotationTest {

@Min(0)
@Max(999)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
public @interface ValidStoreId {
String message() default "Invalid store ID";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

@Size(min = 1, max = 50)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
public @interface ValidName {
String message() default "Invalid name";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

@Pattern(regexp = "^[a-z0-9._%+\\-]+@[a-z0-9.\\-]+\\.[a-z]{2,}$")
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
public @interface ValidEmail {
String message() default "Invalid email";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

static class TestStoreDto {
@Min(0)
@Max(999)
@NotNull
private Short storeId;

@ValidStoreId
@NotNull
private Short metaStoreId;

@ValidName
private String name;

@ValidEmail
private String email;

public Short getStoreId() { return storeId; }
public void setStoreId(Short storeId) { this.storeId = storeId; }
public Short getMetaStoreId() { return metaStoreId; }
public void setMetaStoreId(Short metaStoreId) { this.metaStoreId = metaStoreId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}

@Test
public void readsComposedMinMaxConstraintOnDtoField() {
Map<String, Schema> schemas = ModelConverters.getInstance().readAll(TestStoreDto.class);
Schema model = schemas.get("TestStoreDto");
assertNotNull(model, "Model should be resolved");
Schema meta = (Schema) model.getProperties().get("metaStoreId");
assertNotNull(meta, "metaStoreId property should exist");
assertEquals(((IntegerSchema) meta).getMinimum().intValue(), 0);
assertEquals(((IntegerSchema) meta).getMaximum().intValue(), 999);
}

@Test
public void dtoFieldParityWithDirectAnnotations() {
Map<String, Schema> schemas = ModelConverters.getInstance().readAll(TestStoreDto.class);
Schema model = schemas.get("TestStoreDto");
Schema direct = (Schema) model.getProperties().get("storeId");
Schema meta = (Schema) model.getProperties().get("metaStoreId");
assertEquals(meta.getMinimum(), direct.getMinimum(), "minimum should match direct annotation");
assertEquals(meta.getMaximum(), direct.getMaximum(), "maximum should match direct annotation");
}

@Test
public void readsComposedSizeConstraintOnDtoField() {
Map<String, Schema> schemas = ModelConverters.getInstance().readAll(TestStoreDto.class);
Schema model = schemas.get("TestStoreDto");
Schema name = (Schema) model.getProperties().get("name");
assertNotNull(name, "name property should exist");
assertEquals((int) name.getMinLength(), 1);
assertEquals((int) name.getMaxLength(), 50);
}

@Test
public void readsComposedPatternConstraintOnDtoField() {
Map<String, Schema> schemas = ModelConverters.getInstance().readAll(TestStoreDto.class);
Schema model = schemas.get("TestStoreDto");
Schema email = (Schema) model.getProperties().get("email");
assertNotNull(email, "email property should exist");
assertNotNull(email.getPattern(), "pattern should be set");
}
}
Loading