From 817cd06c767db58281ef765df568f78f39031433 Mon Sep 17 00:00:00 2001 From: Michael Arndt Date: Sat, 14 Dec 2024 13:29:28 +0100 Subject: [PATCH] 4799: Support Composed Constraints --- .../v3/core/jackson/ModelResolver.java | 353 ++++++++++++------ .../core/oas/models/BeanValidationsModel.java | 33 ++ .../v3/core/resolving/BeanValidatorTest.java | 5 + .../resolving/v31/ModelResolverOAS31Test.java | 80 ++++ 4 files changed, 350 insertions(+), 121 deletions(-) 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..1643073c7c 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; @@ -67,6 +66,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.validation.Constraint; import javax.validation.constraints.DecimalMax; import javax.validation.constraints.DecimalMin; import javax.validation.constraints.Max; @@ -89,6 +89,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; import java.math.BigDecimal; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -100,6 +101,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Queue; import java.util.Set; import java.util.Objects; import java.util.stream.Collectors; @@ -316,7 +318,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context model = primitiveType.createProperty(); isPrimitive = true; } - } + } if (model == null) { PrimitiveType primitiveType = PrimitiveType.fromType(type); @@ -367,7 +369,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context if (xml != null) { model.xml(xml); } - if (!type.isEnumType()){ + if (!type.isEnumType()) { applyBeanValidatorAnnotations(model, annotatedType.getCtxAnnotations(), null, false); } resolveSchemaMembers(model, annotatedType, context, next); @@ -412,7 +414,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context Type jsonValueType = findJsonValueType(beanDesc); - if(jsonValueType != null) { + if (jsonValueType != null) { AnnotatedType aType = new AnnotatedType() .type(jsonValueType) .parent(annotatedType.getParent()) @@ -548,9 +550,9 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context } } else if (isComposedSchema) { model = new ComposedSchema().name(name); - if (openapi31 && resolvedArrayAnnotation == null){ + if (openapi31 && resolvedArrayAnnotation == null) { model.addType("object"); - }else{ + } else { model.type("object"); } } else { @@ -560,9 +562,9 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context return model; } else { model = new Schema().name(name); - if (openapi31 && resolvedArrayAnnotation == null){ + if (openapi31 && resolvedArrayAnnotation == null) { model.addType("object"); - }else{ + } else { model.type("object"); } } @@ -612,7 +614,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context // hack to avoid clobbering properties with get/is names // it's ugly but gets around https://github.com/swagger-api/swagger-core/issues/415 - if(propDef.getPrimaryMember() != null) { + if (propDef.getPrimaryMember() != null) { final JsonProperty jsonPropertyAnn = propDef.getPrimaryMember().getAnnotation(JsonProperty.class); if (jsonPropertyAnn == null || !jsonPropertyAnn.value().equals(propName)) { if (member != null) { @@ -646,18 +648,18 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context annotations = annotationList.toArray(new Annotation[annotationList.size()]); - if(hiddenByJsonView(annotations, annotatedType)) { + if (hiddenByJsonView(annotations, annotatedType)) { continue; } JavaType propType = member.getType(); - if(propType != null && "void".equals(propType.getRawClass().getName())) { + if (propType != null && "void".equals(propType.getRawClass().getName())) { if (member instanceof AnnotatedMethod) { - propType = ((AnnotatedMethod)member).getParameterType(0); + propType = ((AnnotatedMethod) member).getParameterType(0); } - } - + } + String propSchemaName = null; io.swagger.v3.oas.annotations.media.Schema ctxSchema = AnnotationsUtils.getSchemaAnnotation(annotations); if (AnnotationsUtils.hasSchemaAnnotation(ctxSchema)) { @@ -733,9 +735,9 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context JsonUnwrapped uw = propMember.getAnnotation(JsonUnwrapped.class); if (uw != null && uw.enabled()) { t - .ctxAnnotations(null) - .jsonUnwrappedHandler(null) - .resolveAsRef(false); + .ctxAnnotations(null) + .jsonUnwrappedHandler(null) + .resolveAsRef(false); handleUnwrapped(props, context.resolve(t), uw.prefix(), uw.suffix(), requiredProps); return null; } else { @@ -875,7 +877,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context } } - for(String propName : requiredProps) { + for (String propName : requiredProps) { addRequiredItem(model, propName); } } @@ -1052,9 +1054,8 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context Schema.SchemaResolution resolvedSchemaResolution = AnnotationsUtils.resolveSchemaResolution(this.schemaResolution, resolvedSchemaAnnotation); if (model != null && annotatedType.isResolveAsRef() && - (isComposedSchema || isObjectSchema(model)) && - StringUtils.isNotBlank(model.getName())) - { + (isComposedSchema || isObjectSchema(model)) && + StringUtils.isNotBlank(model.getName())) { if (context.getDefinedModels().containsKey(model.getName())) { if (!Schema.SchemaResolution.INLINE.equals(resolvedSchemaResolution)) { model = new Schema().$ref(constructRef(model.getName())); @@ -1086,10 +1087,10 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context private Annotation[] addGenericTypeArgumentAnnotationsForOptionalField(BeanPropertyDefinition propDef, Annotation[] annotations) { boolean isNotOptionalType = Optional.ofNullable(propDef) - .map(BeanPropertyDefinition::getField) - .map(AnnotatedField::getAnnotated) - .map(field -> !(field.getType().equals(Optional.class))) - .orElse(false); + .map(BeanPropertyDefinition::getField) + .map(AnnotatedField::getAnnotated) + .map(field -> !(field.getType().equals(Optional.class))) + .orElse(false); if (isNotOptionalType || isRecordType(propDef)) { return annotations; @@ -1100,14 +1101,14 @@ private Annotation[] addGenericTypeArgumentAnnotationsForOptionalField(BeanPrope } private Stream extractGenericTypeArgumentAnnotations(BeanPropertyDefinition propDef) { - if (isRecordType(propDef)){ + if (isRecordType(propDef)) { return getRecordComponentAnnotations(propDef); } return Optional.ofNullable(propDef) - .map(BeanPropertyDefinition::getField) - .map(AnnotatedField::getAnnotated) - .map(this::getGenericTypeArgumentAnnotations) - .orElseGet(Stream::of); + .map(BeanPropertyDefinition::getField) + .map(AnnotatedField::getAnnotated) + .map(this::getGenericTypeArgumentAnnotations) + .orElseGet(Stream::of); } private Stream getRecordComponentAnnotations(BeanPropertyDefinition propDef) { @@ -1120,7 +1121,7 @@ private Stream getRecordComponentAnnotations(BeanPropertyDefinition } } - private Boolean isRecordType(BeanPropertyDefinition propDef){ + private Boolean isRecordType(BeanPropertyDefinition propDef) { try { if (propDef.getPrimaryMember() != null) { Class clazz = propDef.getPrimaryMember().getDeclaringClass(); @@ -1184,7 +1185,7 @@ private Schema clone(Schema property) { private boolean isSubtype(AnnotatedClass childClass, Class parentClass) { final BeanDescription parentDesc = _mapper.getSerializationConfig().introspectClassAnnotations(parentClass); - List subTypes =_intr.findSubtypes(parentDesc.getClassInfo()); + List subTypes = _intr.findSubtypes(parentDesc.getClassInfo()); if (subTypes == null) { return false; } @@ -1207,7 +1208,7 @@ protected boolean _isOptionalType(JavaType propType) { * Adds each enum property value to the model schema * * @param propClass the enum class for which to add properties - * @param property the schema to add properties to + * @param property the schema to add properties to */ protected void _addEnumProps(Class propClass, Schema property) { final boolean useIndex = _mapper.isEnabled(SerializationFeature.WRITE_ENUMS_USING_INDEX); @@ -1232,7 +1233,7 @@ protected void _addEnumProps(Class propClass, Schema property) { if (enumConstants != null) { String[] enumValues = _intr.findEnumValues(propClass, enumConstants, - new String[enumConstants.length]); + new String[enumConstants.length]); for (Enum en : enumConstants) { String n; @@ -1268,13 +1269,13 @@ protected void _addEnumProps(Class propClass, Schema property) { } protected boolean ignore(final Annotated member, final XmlAccessorType xmlAccessorTypeAnnotation, final String propName, final Set propertiesToIgnore) { - return ignore (member, xmlAccessorTypeAnnotation, propName, propertiesToIgnore, null); + return ignore(member, xmlAccessorTypeAnnotation, propName, propertiesToIgnore, null); } protected boolean hasHiddenAnnotation(Annotated annotated) { return annotated.hasAnnotation(Hidden.class) || ( annotated.hasAnnotation(io.swagger.v3.oas.annotations.media.Schema.class) && - annotated.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class).hidden() + annotated.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class).hidden() ); } @@ -1323,7 +1324,7 @@ private void handleUnwrapped(List props, Schema innerModel, String prefi if (StringUtils.isBlank(suffix) && StringUtils.isBlank(prefix)) { if (innerModel.getProperties() != null) { props.addAll(innerModel.getProperties().values()); - if(innerModel.getRequired() != null) { + if (innerModel.getRequired() != null) { requiredProps.addAll(innerModel.getRequired()); } @@ -1450,6 +1451,7 @@ private final class UUIDGeneratorWrapper extends GeneratorWrapper.Base generator) { super(generator); } + @Override protected Schema processAsProperty(String propertyName, AnnotatedType type, ModelConverterContext context, ObjectMapper mapper) { @@ -1501,8 +1503,8 @@ protected abstract Schema processAsId(String propertyName, AnnotatedType type, } public Schema processJsonIdentity(AnnotatedType type, ModelConverterContext context, - ObjectMapper mapper, JsonIdentityInfo identityInfo, - JsonIdentityReference identityReference) { + ObjectMapper mapper, JsonIdentityInfo identityInfo, + JsonIdentityReference identityReference) { final GeneratorWrapper.Base wrapper = identityInfo != null ? getWrapper(identityInfo.generator()) : null; if (wrapper == null) { return null; @@ -1515,30 +1517,31 @@ public Schema processJsonIdentity(AnnotatedType type, ModelConverterContext cont } private GeneratorWrapper.Base getWrapper(Class generator) { - if(ObjectIdGenerators.PropertyGenerator.class.isAssignableFrom(generator)) { + if (ObjectIdGenerators.PropertyGenerator.class.isAssignableFrom(generator)) { return new PropertyGeneratorWrapper(generator); - } else if(ObjectIdGenerators.IntSequenceGenerator.class.isAssignableFrom(generator)) { + } else if (ObjectIdGenerators.IntSequenceGenerator.class.isAssignableFrom(generator)) { return new IntGeneratorWrapper(generator); - } else if(ObjectIdGenerators.UUIDGenerator.class.isAssignableFrom(generator)) { + } else if (ObjectIdGenerators.UUIDGenerator.class.isAssignableFrom(generator)) { return new UUIDGeneratorWrapper(generator); - } else if(ObjectIdGenerators.None.class.isAssignableFrom(generator)) { + } else if (ObjectIdGenerators.None.class.isAssignableFrom(generator)) { return new NoneGeneratorWrapper(generator); } return null; } protected Schema process(Schema id, String propertyName, AnnotatedType type, - ModelConverterContext context) { + ModelConverterContext context) { type = removeJsonIdentityAnnotations(type); Schema model = context.resolve(type); - if(model == null){ - model = resolve(type,context, null); + if (model == null) { + model = resolve(type, context, null); } model.addProperties(propertyName, id); return new Schema().$ref(StringUtils.isNotEmpty(model.get$ref()) ? model.get$ref() : model.getName()); } + private AnnotatedType removeJsonIdentityAnnotations(AnnotatedType type) { return new AnnotatedType() .jsonUnwrappedHandler(type.getJsonUnwrappedHandler()) @@ -1567,59 +1570,71 @@ protected void applyBeanValidatorAnnotations(BeanPropertyDefinition propDef, Sch } protected void applyBeanValidatorAnnotations(Schema property, Annotation[] annotations, Schema parent, boolean applyNotNullAnnotations) { + if (annotations == null) { + return; + } Map annos = new HashMap<>(); - if (annotations != null) { - for (Annotation anno : annotations) { - annos.put(anno.annotationType().getName(), anno); - } + Annotation[] resolvedAnnotations = resolveComposedConstraints(annotations); + for (Annotation anno : resolvedAnnotations) { + 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 = Arrays.stream(resolvedAnnotations).anyMatch(annotation -> NOT_NULL_ANNOTATIONS.contains(annotation.annotationType().getSimpleName()) ); if (requiredItem) { addRequiredItem(parent, property.getName()); } } - if (annos.containsKey("javax.validation.constraints.Min")) { - if (isNumberSchema(property)) { - Min min = (Min) annos.get("javax.validation.constraints.Min"); - property.setMinimum(new BigDecimal(min.value())); - } - } - if (annos.containsKey("javax.validation.constraints.Max")) { - if (isNumberSchema(property)) { - Max max = (Max) annos.get("javax.validation.constraints.Max"); - property.setMaximum(new BigDecimal(max.value())); - } - } - if (annos.containsKey("javax.validation.constraints.Size")) { - Size size = (Size) annos.get("javax.validation.constraints.Size"); - if (isNumberSchema(property)) { - property.setMinimum(new BigDecimal(size.min())); - property.setMaximum(new BigDecimal(size.max())); - } - if (isStringSchema(property)) { - property.setMinLength(Integer.valueOf(size.min())); - property.setMaxLength(Integer.valueOf(size.max())); - } - if (isArraySchema(property)) { - property.setMinItems(size.min()); - property.setMaxItems(size.max()); - } - } - if (annos.containsKey("javax.validation.constraints.DecimalMin")) { - DecimalMin min = (DecimalMin) annos.get("javax.validation.constraints.DecimalMin"); - if (isNumberSchema(property)) { - property.setMinimum(new BigDecimal(min.value())); - property.setExclusiveMinimum(!min.inclusive()); - } - } - if (annos.containsKey("javax.validation.constraints.DecimalMax")) { - DecimalMax max = (DecimalMax) annos.get("javax.validation.constraints.DecimalMax"); - if (isNumberSchema(property)) { - property.setMaximum(new BigDecimal(max.value())); - property.setExclusiveMaximum(!max.inclusive()); + for (Annotation anno : resolvedAnnotations) { + switch (anno.annotationType().getSimpleName()) { + case "javax.validation.constraints.Min": + if (isNumberSchema(property)) { + Min min = (Min) anno; + BigDecimal newMinimum = new BigDecimal(min.value()); + if (isStricterMinimum(property.getMinimum(), newMinimum)) { + property.setMinimum(newMinimum); + } + } + break; + case "javax.validation.constraints.Max": + if (isNumberSchema(property)) { + Max max = (Max) anno; + BigDecimal newMaximum = new BigDecimal(max.value()); + if (isStricterMaximum(property.getMaximum(), newMaximum)) { + property.setMinimum(newMaximum); + } + } + break; + case "javax.validation.constraints.Size": + Size size = (Size) anno; + if (isNumberSchema(property)) { + tightenMinimum(property, new BigDecimal(size.min())); + tightenMaximum(property, new BigDecimal(size.max())); + } + if (isStringSchema(property)) { + tightenMinLength(property, size.min()); + tightenMaxLength(property, size.max()); + } + if (isArraySchema(property)) { + tightenMinItems(property, size.min()); + tightenMaxItems(property, size.max()); + } + break; + case "javax.validation.constraints.DecimalMin": + DecimalMin min = (DecimalMin) anno; + if (isNumberSchema(property)) { + tightenMinimum(property, new BigDecimal(min.value()), !min.inclusive()); + } + break; + case "javax.validation.constraints.DecimalMax": + DecimalMax max = (DecimalMax) anno; + if (isNumberSchema(property) && tightenMaximum(property, new BigDecimal(max.value()))) { + property.setExclusiveMaximum(!max.inclusive()); + } + break; + case "javax.validation.constraints.Pattern": + break; } } if (annos.containsKey("javax.validation.constraints.Pattern")) { @@ -1633,6 +1648,102 @@ protected void applyBeanValidatorAnnotations(Schema property, Annotation[] annot } } + private static boolean isStricterMinimum(BigDecimal oldMinimum, BigDecimal newMinimum) { + if (oldMinimum == null) { + return true; + } + + return oldMinimum.compareTo(newMinimum) < 0; + } + + private static boolean isStricterMaximum(BigDecimal oldMaximum, BigDecimal newMaximum) { + if (oldMaximum == null) { + return true; + } + + return oldMaximum.compareTo(newMaximum) > 0; + } + + private static boolean tightenMinimum(Schema property, BigDecimal minimum) { + return tightenMinimum(property, minimum, false); + } + + private static void tightenMinimum(Schema property, BigDecimal minimum, boolean isExclusive) { + BigDecimal oldMinimum = property.getMinimum(); + + int compareTo = 1; + if (isExclusive) { + compareTo = 0; + } + + if (oldMinimum == null || oldMinimum.compareTo(minimum) < compareTo) { + property.setMinimum(minimum); + property.setExclusiveMinimum(true); + } + } + + private static boolean tightenMaximum(Schema property, BigDecimal maximum) { + BigDecimal oldMaximum = property.getMaximum(); + if (oldMaximum != null) { + maximum = oldMaximum.min(maximum); + property.setMaximum(maximum); + return true; + } + property.setMaximum(maximum); + return false; + } + + private static void tightenMinLength(Schema property, Integer minLength) { + Integer oldMinLength = property.getMinLength(); + if (oldMinLength != null) { + minLength = Integer.max(oldMinLength, minLength); + } + property.setMinLength(minLength); + } + + private static void tightenMaxLength(Schema property, Integer maxLength) { + Integer oldMaxLength = property.getMaxLength(); + if (oldMaxLength != null) { + maxLength = Integer.min(oldMaxLength, maxLength); + } + property.setMaxLength(maxLength); + } + + private static void tightenMinItems(Schema property, Integer minItems) { + Integer oldMinItems = property.getMinItems(); + if (oldMinItems != null) { + minItems = Integer.max(oldMinItems, minItems); + } + property.setMinItems(minItems); + } + + private static void tightenMaxItems(Schema property, Integer maxItems) { + Integer oldMaxItems = property.getMaxItems(); + if (oldMaxItems != null) { + maxItems = Integer.min(oldMaxItems, maxItems); + } + property.setMaxItems(maxItems); + } + + private Annotation[] resolveComposedConstraints(Annotation[] annotations) { + final Queue toResolve = new ArrayDeque<>(Arrays.asList(annotations)); + final List resolved = new ArrayList<>(); + while (!toResolve.isEmpty()) { + final Annotation annotation = toResolve.remove(); + resolved.add(annotation); + Class annotationType = annotation.annotationType(); + Constraint constraint = annotationType.getAnnotation(Constraint.class); + if (constraint == null) { + continue; + } + + toResolve.addAll(Arrays.asList(annotationType.getAnnotations())); + } + + Collections.reverse(resolved); + return resolved.toArray(new Annotation[0]); + } + private boolean resolveSubtypes(Schema model, BeanDescription bean, ModelConverterContext context, JsonView jsonViewAnnotation) { final List types = _intr.findSubtypes(bean.getClassInfo()); if (types == null) { @@ -1662,9 +1773,9 @@ private boolean resolveSubtypes(Schema model, BeanDescription bean, ModelConvert } final Schema subtypeModel = context.resolve(new AnnotatedType().type(subtypeType) - .jsonViewAnnotation(jsonViewAnnotation)); + .jsonViewAnnotation(jsonViewAnnotation)); - if ( StringUtils.isBlank(subtypeModel.getName()) || + if (StringUtils.isBlank(subtypeModel.getName()) || subtypeModel.getName().equals(model.getName())) { subtypeModel.setName(_typeNameResolver.nameForType(_mapper.constructType(subtypeType), TypeNameResolver.Options.SKIP_API_MODEL)); @@ -1746,7 +1857,7 @@ private boolean resolveSubtypes(Schema model, BeanDescription bean, ModelConvert } private void removeSelfFromSubTypes(List types, BeanDescription bean) { - Class beanClass= bean.getType().getRawClass(); + Class beanClass = bean.getType().getRawClass(); types.removeIf(type -> beanClass.equals(type.getType())); } @@ -1840,7 +1951,7 @@ protected Map resolvePatternProperties(JavaType a, Annotation[] PatternProperties props = a.getRawClass().getAnnotation(PatternProperties.class); if (props != null && props.value().length > 0) { - for (PatternProperty prop: props.value()) { + for (PatternProperty prop : props.value()) { propList.put(prop.regex(), prop); } } @@ -1850,7 +1961,7 @@ protected Map resolvePatternProperties(JavaType a, Annotation[] } props = AnnotationsUtils.getAnnotation(PatternProperties.class, annotations); if (props != null && props.value().length > 0) { - for (PatternProperty prop: props.value()) { + for (PatternProperty prop : props.value()) { propList.put(prop.regex(), prop); } } @@ -1865,7 +1976,7 @@ protected Map resolvePatternProperties(JavaType a, Annotation[] Map patternProperties = new LinkedHashMap<>(); - for (PatternProperty prop: propList.values()) { + for (PatternProperty prop : propList.values()) { String key = prop.regex(); if (StringUtils.isBlank(key)) { continue; @@ -1889,7 +2000,7 @@ protected Map resolveSchemaProperties(JavaType a, Annotation[] a SchemaProperties props = a.getRawClass().getAnnotation(SchemaProperties.class); if (props != null && props.value().length > 0) { - for (SchemaProperty prop: props.value()) { + for (SchemaProperty prop : props.value()) { propList.put(prop.name(), prop); } } @@ -1899,7 +2010,7 @@ protected Map resolveSchemaProperties(JavaType a, Annotation[] a } props = AnnotationsUtils.getAnnotation(SchemaProperties.class, annotations); if (props != null && props.value().length > 0) { - for (SchemaProperty prop: props.value()) { + for (SchemaProperty prop : props.value()) { propList.put(prop.name(), prop); } } @@ -1914,7 +2025,7 @@ protected Map resolveSchemaProperties(JavaType a, Annotation[] a Map schemaProperties = new LinkedHashMap<>(); - for (SchemaProperty prop: propList.values()) { + for (SchemaProperty prop : propList.values()) { String key = prop.name(); if (StringUtils.isBlank(key)) { continue; @@ -1965,7 +2076,7 @@ protected Map resolveDependentSchemas(JavaType a, Annotation[] a Map dependentSchemas = new LinkedHashMap<>(); - for (DependentSchema dependentSchemaAnnotation: dependentSchemaMap.values()) { + for (DependentSchema dependentSchemaAnnotation : dependentSchemaMap.values()) { String name = dependentSchemaAnnotation.name(); if (StringUtils.isBlank(name)) { continue; @@ -2003,9 +2114,9 @@ protected Object resolveDefaultValue(Annotated a, Annotation[] annotations, io.s XmlElement elem = a.getAnnotation(XmlElement.class); if (elem == null) { if (annotations != null) { - for (Annotation ann: annotations) { + for (Annotation ann : annotations) { if (ann instanceof XmlElement) { - elem = (XmlElement)ann; + elem = (XmlElement) ann; break; } } @@ -2324,7 +2435,7 @@ protected Schema resolveWrapping(JavaType type, ModelConverterContext context, S if (JsonTypeInfo.Id.NAME.equals(id) && typeName != null) { name = typeName.value(); } - if(JsonTypeInfo.Id.NAME.equals(id) && name == null) { + if (JsonTypeInfo.Id.NAME.equals(id) && name == null) { name = type.getRawClass().getSimpleName(); } Schema wrapperSchema = new ObjectSchema(); @@ -2384,9 +2495,9 @@ protected XML resolveXml(Annotated a, Annotation[] annotations, io.swagger.v3.oa } if (rootAnnotation == null) { if (annotations != null) { - for (Annotation ann: annotations) { + for (Annotation ann : annotations) { if (ann instanceof XmlRootElement) { - rootAnnotation = (XmlRootElement)ann; + rootAnnotation = (XmlRootElement) ann; break; } } @@ -2416,9 +2527,9 @@ protected Set resolveIgnoredProperties(Annotations a, Annotation[] annot Set propertiesToIgnore = new HashSet<>(); JsonIgnoreProperties ignoreProperties = a.get(JsonIgnoreProperties.class); if (ignoreProperties != null) { - if(!ignoreProperties.allowGetters()) { - propertiesToIgnore.addAll(Arrays.asList(ignoreProperties.value())); - } + if (!ignoreProperties.allowGetters()) { + propertiesToIgnore.addAll(Arrays.asList(ignoreProperties.value())); + } } propertiesToIgnore.addAll(resolveIgnoredProperties(annotations)); return propertiesToIgnore; @@ -2557,7 +2668,7 @@ protected String resolveContentMediaType(Annotated a, Annotation[] annotations, if (schema != null && StringUtils.isNotBlank(schema.contentMediaType())) { return schema.contentMediaType(); } - return null; + return null; } protected void resolveContains(AnnotatedType annotatedType, ArraySchema arraySchema, io.swagger.v3.oas.annotations.media.ArraySchema arraySchemaAnnotation) { @@ -2608,7 +2719,7 @@ protected Map> resolveDependentRequired(Annotated a, Annota return null; } final Map> dependentRequiredMap = new HashMap<>(); - for (DependentRequired dependentRequired: schema.dependentRequiredMap()) { + for (DependentRequired dependentRequired : schema.dependentRequiredMap()) { final String name = dependentRequired.name(); if (dependentRequired.value().length == 0) { continue; @@ -2745,14 +2856,14 @@ protected void resolveSchemaMembers(Schema schema, AnnotatedType annotatedType, } } - final Map> dependentRequired = resolveDependentRequired(a, annotations, schemaAnnotation); + final Map> dependentRequired = resolveDependentRequired(a, annotations, schemaAnnotation); if (dependentRequired != null && !dependentRequired.isEmpty()) { schema.setDependentRequired(dependentRequired); } final Map dependentSchemas = resolveDependentSchemas(a, annotations, schemaAnnotation, annotatedType, context, next); if (dependentSchemas != null) { final Map processedDependentSchemas = new LinkedHashMap<>(); - for (String key: dependentSchemas.keySet()) { + for (String key : dependentSchemas.keySet()) { Schema val = dependentSchemas.get(key); processedDependentSchemas.put(key, buildRefSchemaIfObject(val, context)); } @@ -2978,7 +3089,7 @@ private List getIgnoredProperties(BeanDescription beanDescription) { } /** - * Decorate the name based on the JsonView + * Decorate the name based on the JsonView */ protected String decorateModelName(AnnotatedType type, String originalName) { if (StringUtils.isBlank(originalName)) { @@ -2998,7 +3109,7 @@ protected String decorateModelName(AnnotatedType type, String originalName) { } protected boolean hiddenByJsonView(Annotation[] annotations, - AnnotatedType type) { + AnnotatedType type) { JsonView jsonView = type.getJsonViewAnnotation(); if (jsonView == null) return false; @@ -3091,19 +3202,19 @@ protected boolean isObjectSchema(Schema schema) { return (schema.getTypes() != null && schema.getTypes().contains("object")) || "object".equals(schema.getType()) || (schema.getType() == null && schema.getProperties() != null && !schema.getProperties().isEmpty()); } - protected boolean isArraySchema(Schema schema){ + protected boolean isArraySchema(Schema schema) { return "array".equals(schema.getType()) || (schema.getTypes() != null && schema.getTypes().contains("array")); } - protected boolean isStringSchema(Schema schema){ + protected boolean isStringSchema(Schema schema) { return "string".equals(schema.getType()) || (schema.getTypes() != null && schema.getTypes().contains("string")); } - protected boolean isNumberSchema(Schema schema){ + protected boolean isNumberSchema(Schema schema) { return "number".equals(schema.getType()) || (schema.getTypes() != null && schema.getTypes().contains("number")) || "integer".equals(schema.getType()) || (schema.getTypes() != null && schema.getTypes().contains("integer")); } - private AnnotatedMember invokeMethod(final BeanDescription beanDesc, String methodName) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException{ + private AnnotatedMember invokeMethod(final BeanDescription beanDesc, String methodName) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { Method m = BeanDescription.class.getMethod(methodName); return (AnnotatedMember) m.invoke(beanDesc); } @@ -3123,7 +3234,7 @@ protected Schema buildRefSchemaIfObject(Schema schema, ModelConverterContext con protected boolean applySchemaResolution() { return !openapi31 || - (Boolean.parseBoolean(System.getProperty(Schema.APPLY_SCHEMA_RESOLUTION_PROPERTY, "false")) || - Boolean.parseBoolean(System.getenv(Schema.APPLY_SCHEMA_RESOLUTION_PROPERTY))); + (Boolean.parseBoolean(System.getProperty(Schema.APPLY_SCHEMA_RESOLUTION_PROPERTY, "false")) || + Boolean.parseBoolean(System.getenv(Schema.APPLY_SCHEMA_RESOLUTION_PROPERTY))); } } diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/BeanValidationsModel.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/BeanValidationsModel.java index ce75707817..1337bfa6de 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/BeanValidationsModel.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/BeanValidationsModel.java @@ -1,5 +1,8 @@ package io.swagger.v3.core.oas.models; +import io.swagger.v3.core.resolving.v31.ModelResolverOAS31Test; + +import javax.validation.Constraint; import javax.validation.constraints.DecimalMax; import javax.validation.constraints.DecimalMin; import javax.validation.constraints.Max; @@ -7,9 +10,16 @@ import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.util.List; import java.util.Optional; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + public class BeanValidationsModel { @NotNull protected Long id; @@ -18,6 +28,9 @@ public class BeanValidationsModel { @Max(99) protected Integer age; + @DeeplyComposedConstraint + protected String lastName; + protected String username; @Size(min = 6, max = 20) @@ -57,6 +70,14 @@ public void setAge(Integer age) { this.age = age; } + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + public String getUsername() { return username; } @@ -129,4 +150,16 @@ public void setOptionalValue(Optional optionalValue) { this.optionalValue = optionalValue; } + @Target({FIELD, ANNOTATION_TYPE, TYPE_USE }) + @Retention(RUNTIME) + @Constraint(validatedBy = {}) + @Pattern(regexp = "[A-Z].*") + private @interface ComposedConstraint {} + + @Target({ FIELD, ANNOTATION_TYPE, TYPE_USE }) + @Retention(RUNTIME) + @Constraint(validatedBy = {}) + @ComposedConstraint + @Size(min = 1, max = 100) + private @interface DeeplyComposedConstraint {} } diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/BeanValidatorTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/BeanValidatorTest.java index 9a00b4b6ef..51353986d5 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/BeanValidatorTest.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/BeanValidatorTest.java @@ -27,6 +27,11 @@ public void readBeanValidatorTest() { Assert.assertEquals(age.getMinimum(), new BigDecimal(13.0)); Assert.assertEquals(age.getMaximum(), new BigDecimal(99.0)); + final StringSchema lastName = (StringSchema) properties.get("lastName"); + Assert.assertEquals((int) lastName.getMinLength(), 1); + Assert.assertEquals((int) lastName.getMaxLength(), 100); + Assert.assertEquals(lastName.getPattern(), "[A-Z].*"); + final StringSchema password = (StringSchema) properties.get("password"); Assert.assertEquals((int) password.getMinLength(), 6); Assert.assertEquals((int) password.getMaxLength(), 20); diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/ModelResolverOAS31Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/ModelResolverOAS31Test.java index 12e2ca78fc..de66d78308 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/ModelResolverOAS31Test.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/ModelResolverOAS31Test.java @@ -13,13 +13,21 @@ import io.swagger.v3.core.resolving.v31.model.ModelWithOAS31Stuff; import io.swagger.v3.oas.models.media.Schema; import org.testng.annotations.Test; +import javax.validation.Constraint; import javax.validation.constraints.DecimalMax; import javax.validation.constraints.DecimalMin; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.util.List; import java.util.Map; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + public class ModelResolverOAS31Test extends SwaggerTestBase { @Test @@ -264,6 +272,35 @@ public void setMyField(List myField) { } } + @Test(description = "Composed Constraint handled in type parameters of properties using collections when using oas 3.1.0") + public void testModelUsingCollectionTypePropertyHandlesComposedConstraintAnnotationForOas31() { + String expectedYaml = "ClassWithUsingComposedConstraintOnCollection:\n" + + " type: object\n" + + " properties:\n" + + " myField:\n" + + " type: array\n" + + " items:\n" + + " maxLength: 100\n" + + " minLength: 1\n" + + " pattern: myTopLevelPattern\n" + + " type: string"; + + Map stringSchemaMap = ModelConverters.getInstance(true).readAll(ClassWithUsingComposedConstraintOnCollection.class); + SerializationMatchers.assertEqualsToYaml31(stringSchemaMap, expectedYaml); + } + + private static class ClassWithUsingComposedConstraintOnCollection { + private List<@DeeplyComposedConstraint @Pattern(regexp = "myTopLevelPattern") String> myField; + + public List getMyField() { + return myField; + } + + public void setMyField(List myField) { + this.myField = myField; + } + } + @Test(description = "@Size correctly handled in properties using collections when using oas 3.1.0") public void testModelUsingCollectionTypePropertyHandleSizeAnnotationForOas31() { String expectedYaml = "ClassWithUsingSizeOnCollection:\n" + @@ -347,4 +384,47 @@ public void setMyField(Number myField) { this.myField = myField; } } + + + @Test(description = "Composed Constraints are handled on fields") + public void testComposedConstraintOnField() { + String expectedYaml = "ClassWithUsingComposedAnnotationsOnField:\n" + + " type: object\n" + + " properties:\n" + + " myField:\n" + + " type: string\n" + + " maxLength: 100\n" + + " minLength: 1\n" + + " pattern: myTopLevelPattern"; + + Map stringSchemaMap = ModelConverters.getInstance(true).readAll(ClassWithUsingComposedAnnotationsOnField.class); + SerializationMatchers.assertEqualsToYaml31(stringSchemaMap, expectedYaml); + } + + private static class ClassWithUsingComposedAnnotationsOnField { + @DeeplyComposedConstraint + @Pattern(regexp = "myTopLevelPattern") + private String myField; + + public String getMyField() { + return myField; + } + + public void setMyField(String myField) { + this.myField = myField; + } + } + + @Target({FIELD, ANNOTATION_TYPE, TYPE_USE }) + @Retention(RUNTIME) + @Constraint(validatedBy = {}) + @Pattern(regexp = "myPattern") + private @interface ComposedConstraint {} + + @Target({ FIELD, ANNOTATION_TYPE, TYPE_USE }) + @Retention(RUNTIME) + @Constraint(validatedBy = {}) + @ComposedConstraint + @Size(min = 1, max = 100) + private @interface DeeplyComposedConstraint {} }