ValidationAnnotationsUtils.java
package io.swagger.v3.core.util;
import io.swagger.v3.oas.models.SpecVersion;
import io.swagger.v3.oas.models.media.Schema;
import javax.validation.OverridesAttribute;
import javax.validation.constraints.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import static io.swagger.v3.core.util.SchemaTypeUtils.*;
public class ValidationAnnotationsUtils {
private static final String JAVAX_PACKAGE_BASE = "javax.validation.constraints";
public static final String JAVAX_NOT_NULL = JAVAX_PACKAGE_BASE + ".NotNull";
public static final String JAVAX_NOT_EMPTY = JAVAX_PACKAGE_BASE + ".NotEmpty";
public static final String JAVAX_NOT_BLANK = JAVAX_PACKAGE_BASE + ".NotBlank";
public static final String JAVAX_MIN = JAVAX_PACKAGE_BASE + ".Min";
public static final String JAVAX_MAX = JAVAX_PACKAGE_BASE + ".Max";
public static final String JAVAX_SIZE = JAVAX_PACKAGE_BASE + ".Size";
public static final String JAVAX_DECIMAL_MIN = JAVAX_PACKAGE_BASE + ".DecimalMin";
public static final String JAVAX_DECIMAL_MAX = JAVAX_PACKAGE_BASE + ".DecimalMax";
public static final String JAVAX_PATTERN = JAVAX_PACKAGE_BASE + ".Pattern";
public static final String JAVAX_EMAIL = JAVAX_PACKAGE_BASE + ".Email";
public static final String JAVAX_POSITIVE = JAVAX_PACKAGE_BASE + ".Positive";
public static final String JAVAX_POSITIVE_OR_ZERO = JAVAX_PACKAGE_BASE + ".PositiveOrZero";
public static final String JAVAX_NEGATIVE = JAVAX_PACKAGE_BASE + ".Negative";
public static final String JAVAX_NEGATIVE_OR_ZERO = JAVAX_PACKAGE_BASE + ".NegativeOrZero";
private static final String SCHEMA_EMAIL_FORMAT_NAME = "email";
/**
* @param schema the schema
* @param ctxSchema the schema's {@link io.swagger.v3.oas.annotations.media.Schema} annotation
* @param ctxArraySchema the schema's {@link io.swagger.v3.oas.annotations.media.ArraySchema} annotation
* @return whether the schema has been modified or not
*/
public static boolean applyNotEmptyConstraint(Schema schema,
io.swagger.v3.oas.annotations.media.Schema ctxSchema,
io.swagger.v3.oas.annotations.media.ArraySchema ctxArraySchema) {
if (isArraySchema(schema)) {
if (ctxArraySchema == null || ctxArraySchema.minItems() == Integer.MAX_VALUE) {
schema.setMinItems(1);
return true;
}
} else if (isStringSchema(schema)) {
if (ctxSchema == null || ctxSchema.minLength() == 0) {
schema.setMinLength(1);
return true;
}
} else if (isObjectSchema(schema)) {
if (ctxSchema == null || ctxSchema.minProperties() == 0) {
schema.setMinProperties(1);
return true;
}
}
return false;
}
/**
* @param schema the schema
* @param ctxSchema the schema's {@link io.swagger.v3.oas.annotations.media.Schema} annotation
* @return whether the schema has been modified or not
*/
public static boolean applyNotBlankConstraint(Schema schema, io.swagger.v3.oas.annotations.media.Schema ctxSchema) {
if (isStringSchema(schema)) {
if (ctxSchema == null || ctxSchema.minLength() == 0) {
schema.setMinLength(1);
return true;
}
}
return false;
}
/**
* @param schema the schema
* @param annotation the schema's {@link Min} annotation
* @return whether the schema has been modified or not
*/
public static boolean applyMinConstraint(Schema schema, Min annotation) {
if (isNumberSchema(schema)) {
schema.setMinimum(new BigDecimal(annotation.value()));
return true;
}
return false;
}
/**
* @param schema the schema
* @param annotation the schema's {@link Max} annotation
* @return whether the schema has been modified or not
*/
public static boolean applyMaxConstraint(Schema schema, Max annotation) {
if (isNumberSchema(schema)) {
schema.setMaximum(new BigDecimal(annotation.value()));
return true;
}
return false;
}
/**
* @param schema the schema
* @param annotation the schema's {@link Size} annotation
* @return whether the schema has been modified or not
*/
public static boolean applySizeConstraint(Schema schema, Size annotation) {
if (isNumberSchema(schema)) {
schema.setMinimum(new BigDecimal(annotation.min()));
schema.setMaximum(new BigDecimal(annotation.max()));
return true;
}
if (isStringSchema(schema)) {
schema.setMinLength(annotation.min());
schema.setMaxLength(annotation.max());
return true;
}
if (isArraySchema(schema)) {
schema.setMinItems(annotation.min());
schema.setMaxItems(annotation.max());
return true;
}
return false;
}
/**
* @param schema the schema
* @param annotation the schema's {@link DecimalMin} annotation
* @return whether the schema has been modified or not
*/
public static boolean applyDecimalMinConstraint(Schema schema, DecimalMin annotation) {
if (!isNumberSchema(schema)) {
return false;
}
BigDecimal value = new BigDecimal(annotation.value());
if (schema.getSpecVersion().equals(SpecVersion.V31)) {
if (!annotation.inclusive()) {
schema.setExclusiveMinimumValue(value);
BigDecimal minimum = schema.getMinimum();
if (minimum != null && minimum.compareTo(value) <= 0) {
schema.setMinimum(null);
}
} else {
schema.setMinimum(value);
}
} else {
schema.setMinimum(value);
schema.setExclusiveMinimum(!annotation.inclusive());
}
return true;
}
/**
* @param schema the schema
* @param annotation the schema's {@link DecimalMax} annotation
* @return whether the schema has been modified or not
*/
public static boolean applyDecimalMaxConstraint(Schema schema, DecimalMax annotation) {
if (!isNumberSchema(schema)) {
return false;
}
BigDecimal value = new BigDecimal(annotation.value());
if (schema.getSpecVersion().equals(SpecVersion.V31)) {
if (!annotation.inclusive()) {
schema.setExclusiveMaximumValue(value);
BigDecimal maximum = schema.getMaximum();
if (maximum != null && maximum.compareTo(value) >= 0) {
schema.setMaximum(null);
}
} else {
schema.setMaximum(value);
}
} else {
schema.setMaximum(value);
schema.setExclusiveMaximum(!annotation.inclusive());
}
return true;
}
/**
* @param schema the schema
* @param annotation the schema's {@link Pattern} annotation
* @return whether the schema has been modified or not
*/
public static boolean applyPatternConstraint(Schema schema, Pattern annotation) {
if (isStringSchema(schema)) {
schema.setPattern(annotation.regexp());
return true;
}
if (schema.getItems() != null && isStringSchema(schema.getItems())) {
schema.getItems().setPattern(annotation.regexp());
return true;
}
return false;
}
/**
* @param schema the schema
* @param annotation the schema's {@link Email} annotation
* @return whether the schema has been modified or not
*/
public static boolean applyEmailConstraint(Schema schema, Email annotation) {
if (isStringSchema(schema)) {
schema.setFormat(SCHEMA_EMAIL_FORMAT_NAME);
return true;
}
if (schema.getItems() != null && isStringSchema(schema.getItems())) {
schema.getItems().setFormat(SCHEMA_EMAIL_FORMAT_NAME);
return true;
}
return false;
}
public static boolean applyPositiveConstraint(Schema schema) {
if (!isNumberSchema(schema)) {
return false;
}
if (schema.getSpecVersion().equals(SpecVersion.V30)) {
return applyPositiveConstraintV30(schema);
}
return applyPositiveConstraintV31(schema);
}
private static boolean applyPositiveConstraintV30(Schema schema) {
BigDecimal minimum = schema.getMinimum();
if (currentMinimumOutsidePositiveRange(minimum)) {
schema.setMinimum(BigDecimal.ZERO);
schema.setExclusiveMinimum(true);
} else if (minimum.compareTo(BigDecimal.ZERO) == 0 && !Boolean.TRUE.equals(schema.getExclusiveMinimum())) {
schema.setExclusiveMinimum(true);
}
return true;
}
private static boolean applyPositiveConstraintV31(Schema schema) {
BigDecimal exclusiveMinimum = schema.getExclusiveMinimumValue();
if (exclusiveMinimum != null) {
if (exclusiveMinimum.compareTo(BigDecimal.ZERO) < 0) {
BigDecimal minimum = schema.getMinimum();
if (minimum != null && minimum.compareTo(BigDecimal.ZERO) > 0) {
schema.setExclusiveMinimumValue(null);
} else {
schema.setMinimum(null);
schema.setExclusiveMinimumValue(BigDecimal.ZERO);
}
return true;
}
return false;
}
BigDecimal minimum = schema.getMinimum();
if (minimum != null && minimum.compareTo(BigDecimal.ZERO) > 0) {
return false;
}
schema.setMinimum(null);
schema.setExclusiveMinimumValue(BigDecimal.ZERO);
return true;
}
public static boolean applyPositiveOrZeroConstraint(Schema schema) {
if (isNumberSchema(schema)) {
BigDecimal current = schema.getMinimum();
if (current == null || current.compareTo(BigDecimal.ZERO) < 0) {
schema.setMinimum(BigDecimal.ZERO);
}
return true;
}
return false;
}
public static boolean applyNegativeConstraint(Schema schema) {
if (!isNumberSchema(schema)) {
return false;
}
if (schema.getSpecVersion().equals(SpecVersion.V30)) {
return applyNegativeConstraintV30(schema);
}
return applyNegativeConstraintV31(schema);
}
private static boolean applyNegativeConstraintV30(Schema schema) {
BigDecimal maximum = schema.getMaximum();
if (currentMaximumOutsideNegativeRange(maximum)) {
schema.setMaximum(BigDecimal.ZERO);
schema.setExclusiveMaximum(true);
} else if (maximum.compareTo(BigDecimal.ZERO) == 0 && !Boolean.TRUE.equals(schema.getExclusiveMaximum())) {
schema.setExclusiveMaximum(true);
}
return true;
}
private static boolean applyNegativeConstraintV31(Schema schema) {
BigDecimal exclusiveMaximum = schema.getExclusiveMaximumValue();
if (exclusiveMaximum != null) {
if (exclusiveMaximum.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal maximum = schema.getMaximum();
if (maximum != null && maximum.compareTo(BigDecimal.ZERO) < 0) {
schema.setExclusiveMaximumValue(null);
} else {
schema.setMaximum(null);
schema.setExclusiveMaximumValue(BigDecimal.ZERO);
}
return true;
}
return false;
}
BigDecimal maximum = schema.getMaximum();
if (maximum != null && maximum.compareTo(BigDecimal.ZERO) < 0) {
return false;
}
schema.setMaximum(null);
schema.setExclusiveMaximumValue(BigDecimal.ZERO);
return true;
}
/**
* 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 {
Set<Class<?>> visited = new HashSet<>();
Queue<Annotation> queue = new ArrayDeque<>();
for (Annotation a : annotations) {
if (a != null) queue.add(a);
}
while (!queue.isEmpty()) {
Annotation annotation = queue.poll();
if (!visited.add(annotation.annotationType())) continue;
Set<Class<? extends Annotation>> annotationsThatRelyOnOverride = findOverrides(annotation);
for (Annotation meta : annotation.annotationType().getAnnotations()) {
if (meta == null) continue;
String name = meta.annotationType().getName();
if (name.startsWith(JAVAX_PACKAGE_BASE)) {
if (!annotationsThatRelyOnOverride.contains(meta.annotationType())) {
merged.putIfAbsent(name, meta);
}
} else {
queue.add(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();
if (current == null || current.compareTo(BigDecimal.ZERO) > 0) {
schema.setMaximum(BigDecimal.ZERO);
}
return true;
}
return false;
}
private static boolean currentMinimumOutsidePositiveRange(BigDecimal currentMinimum) {
return currentMinimum == null || currentMinimum.compareTo(BigDecimal.ZERO) < 0;
}
private static boolean currentMaximumOutsideNegativeRange(BigDecimal currentMaximum) {
return currentMaximum == null || currentMaximum.compareTo(BigDecimal.ZERO) > 0;
}
/**
*
* @param annotation the composed constraint annotation
* @return the composing annotations that are overridden with {@link OverridesAttribute}
*/
private static Set<Class<? extends Annotation>> findOverrides(Annotation annotation) {
Set<Class<? extends Annotation>> overriddenConstraintAnnotations = new HashSet<>();
Class<? extends Annotation> type = annotation.annotationType();
for (Method method : type.getDeclaredMethods()) {
for (OverridesAttribute oa : method.getAnnotationsByType(OverridesAttribute.class)) {
if (oa != null) {
overriddenConstraintAnnotations.add(oa.constraint());
}
}
}
return overriddenConstraintAnnotations;
}
}