diff --git a/pom.xml b/pom.xml index 5e40398c..19a5347f 100644 --- a/pom.xml +++ b/pom.xml @@ -55,6 +55,11 @@ JSON Schema (http://tools.ietf.org/html/draft-zyp-json-schema-03) version 3 gene jackson-databind ${version.jackson.core} + + javax.validation + validation-api + 1.1.0.Final + diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/ValidationSchemaFactoryWrapper.java b/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/ValidationSchemaFactoryWrapper.java new file mode 100644 index 00000000..cc756f2c --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/module/jsonSchema/customProperties/ValidationSchemaFactoryWrapper.java @@ -0,0 +1,88 @@ +package com.fasterxml.jackson.module.jsonSchema.customProperties; + +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.module.jsonSchema.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.factories.*; +import com.fasterxml.jackson.module.jsonSchema.types.ArraySchema; +import com.fasterxml.jackson.module.jsonSchema.types.NumberSchema; +import com.fasterxml.jackson.module.jsonSchema.types.ObjectSchema; +import com.fasterxml.jackson.module.jsonSchema.types.StringSchema; +import com.fasterxml.jackson.module.jsonSchema.validation.AnnotationConstraintResolver; +import com.fasterxml.jackson.module.jsonSchema.validation.ValidationConstraintResolver; + +/** + * @author cponomaryov + */ +public class ValidationSchemaFactoryWrapper extends SchemaFactoryWrapper { + + private ValidationConstraintResolver constraintResolver; + + private static class ValidationSchemaFactoryWrapperFactory extends WrapperFactory { + @Override + public SchemaFactoryWrapper getWrapper(SerializerProvider p) { + SchemaFactoryWrapper wrapper = new ValidationSchemaFactoryWrapper(); + wrapper.setProvider(p); + return wrapper; + } + + @Override + public SchemaFactoryWrapper getWrapper(SerializerProvider p, VisitorContext rvc) { + SchemaFactoryWrapper wrapper = new ValidationSchemaFactoryWrapper(); + wrapper.setProvider(p); + wrapper.setVisitorContext(rvc); + return wrapper; + } + } + + public ValidationSchemaFactoryWrapper() { + this(new AnnotationConstraintResolver()); + } + + public ValidationSchemaFactoryWrapper(ValidationConstraintResolver constraintResolver) { + super(new ValidationSchemaFactoryWrapperFactory()); + this.constraintResolver = constraintResolver; + } + + @Override + public JsonObjectFormatVisitor expectObjectFormat(JavaType convertedType) { + return new ObjectVisitorDecorator((ObjectVisitor) super.expectObjectFormat(convertedType)) { + private JsonSchema getPropertySchema(BeanProperty writer) { + return ((ObjectSchema) getSchema()).getProperties().get(writer.getName()); + } + + @Override + public void optionalProperty(BeanProperty writer) throws JsonMappingException { + super.optionalProperty(writer); + addValidationConstraints(getPropertySchema(writer), writer); + } + + @Override + public void property(BeanProperty writer) throws JsonMappingException { + super.property(writer); + addValidationConstraints(getPropertySchema(writer), writer); + } + }; + } + + private JsonSchema addValidationConstraints(JsonSchema schema, BeanProperty prop) { + if (schema.isArraySchema()) { + ArraySchema arraySchema = schema.asArraySchema(); + arraySchema.setMaxItems(constraintResolver.getArrayMaxItems(prop)); + arraySchema.setMinItems(constraintResolver.getArrayMinItems(prop)); + } else if (schema.isNumberSchema()) { + NumberSchema numberSchema = schema.asNumberSchema(); + numberSchema.setMaximum(constraintResolver.getNumberMaximum(prop)); + numberSchema.setMinimum(constraintResolver.getNumberMinimum(prop)); + } else if (schema.isStringSchema()) { + StringSchema stringSchema = schema.asStringSchema(); + stringSchema.setMaxLength(constraintResolver.getStringMaxLength(prop)); + stringSchema.setMinLength(constraintResolver.getStringMinLength(prop)); + } + return schema; + } + +} diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ObjectVisitorDecorator.java b/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ObjectVisitorDecorator.java new file mode 100644 index 00000000..bed7c12b --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/module/jsonSchema/factories/ObjectVisitorDecorator.java @@ -0,0 +1,57 @@ +package com.fasterxml.jackson.module.jsonSchema.factories; + +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.module.jsonSchema.JsonSchema; + +/** + * @author cponomaryov + */ +public class ObjectVisitorDecorator implements JsonObjectFormatVisitor, JsonSchemaProducer { + + protected ObjectVisitor objectVisitor; + + public ObjectVisitorDecorator(ObjectVisitor objectVisitor) { + this.objectVisitor = objectVisitor; + } + + @Override + public JsonSchema getSchema() { + return objectVisitor.getSchema(); + } + + @Override + public SerializerProvider getProvider() { + return objectVisitor.getProvider(); + } + + @Override + public void setProvider(SerializerProvider serializerProvider) { + objectVisitor.setProvider(serializerProvider); + } + + @Override + public void optionalProperty(BeanProperty writer) throws JsonMappingException { + objectVisitor.optionalProperty(writer); + } + + @Override + public void optionalProperty(String name, JsonFormatVisitable handler, JavaType propertyTypeHint) throws JsonMappingException { + objectVisitor.optionalProperty(name, handler, propertyTypeHint); + } + + @Override + public void property(BeanProperty writer) throws JsonMappingException { + objectVisitor.property(writer); + } + + @Override + public void property(String name, JsonFormatVisitable handler, JavaType propertyTypeHint) throws JsonMappingException { + objectVisitor.property(name, handler, propertyTypeHint); + } + +} diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/validation/AnnotationConstraintResolver.java b/src/main/java/com/fasterxml/jackson/module/jsonSchema/validation/AnnotationConstraintResolver.java new file mode 100644 index 00000000..a41df48e --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/module/jsonSchema/validation/AnnotationConstraintResolver.java @@ -0,0 +1,66 @@ +package com.fasterxml.jackson.module.jsonSchema.validation; + +import com.fasterxml.jackson.databind.BeanProperty; + +import javax.validation.constraints.*; +import java.math.BigDecimal; + +/** + * @author cponomaryov + */ +public class AnnotationConstraintResolver implements ValidationConstraintResolver { + + private Size getSizeAnnotation(BeanProperty prop) { + return prop.getAnnotation(Size.class); + } + + private Integer getMaxSize(BeanProperty prop) { + Size sizeAnnotation = getSizeAnnotation(prop); + return sizeAnnotation != null && sizeAnnotation.max() != Integer.MAX_VALUE ? sizeAnnotation.max() : null; + } + + private Integer getMinSize(BeanProperty prop) { + Size sizeAnnotation = getSizeAnnotation(prop); + return sizeAnnotation != null && sizeAnnotation.min() != 0 ? sizeAnnotation.min() : null; + } + + @Override + public Integer getArrayMaxItems(BeanProperty prop) { + return getMaxSize(prop); + } + + @Override + public Integer getArrayMinItems(BeanProperty prop) { + return getMinSize(prop); + } + + @Override + public Double getNumberMaximum(BeanProperty prop) { + Max maxAnnotation = prop.getAnnotation(Max.class); + if (maxAnnotation != null) { + return (double) maxAnnotation.value(); + } + DecimalMax decimalMaxAnnotation = prop.getAnnotation(DecimalMax.class); + return decimalMaxAnnotation != null ? new BigDecimal(decimalMaxAnnotation.value()).doubleValue() : null; + } + + @Override + public Double getNumberMinimum(BeanProperty prop) { + Min minAnnotation = prop.getAnnotation(Min.class); + if (minAnnotation != null) { + return (double) minAnnotation.value(); + } + DecimalMin decimalMinAnnotation = prop.getAnnotation(DecimalMin.class); + return decimalMinAnnotation != null ? new BigDecimal(decimalMinAnnotation.value()).doubleValue() : null; + } + + @Override + public Integer getStringMaxLength(BeanProperty prop) { + return getMaxSize(prop); + } + + @Override + public Integer getStringMinLength(BeanProperty prop) { + return getMinSize(prop); + } +} diff --git a/src/main/java/com/fasterxml/jackson/module/jsonSchema/validation/ValidationConstraintResolver.java b/src/main/java/com/fasterxml/jackson/module/jsonSchema/validation/ValidationConstraintResolver.java new file mode 100644 index 00000000..0ada0e7d --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/module/jsonSchema/validation/ValidationConstraintResolver.java @@ -0,0 +1,22 @@ +package com.fasterxml.jackson.module.jsonSchema.validation; + +import com.fasterxml.jackson.databind.BeanProperty; + +/** + * @author cponomaryov + */ +public interface ValidationConstraintResolver { + + Integer getArrayMaxItems(BeanProperty prop); + + Integer getArrayMinItems(BeanProperty prop); + + Double getNumberMaximum(BeanProperty prop); + + Double getNumberMinimum(BeanProperty prop); + + Integer getStringMaxLength(BeanProperty prop); + + Integer getStringMinLength(BeanProperty prop); + +} diff --git a/src/test/java/com/fasterxml/jackson/module/jsonSchema/ValidationSchemaFactoryWrapperTest.java b/src/test/java/com/fasterxml/jackson/module/jsonSchema/ValidationSchemaFactoryWrapperTest.java new file mode 100644 index 00000000..fc9fcb34 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/module/jsonSchema/ValidationSchemaFactoryWrapperTest.java @@ -0,0 +1,302 @@ +package com.fasterxml.jackson.module.jsonSchema; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jsonSchema.customProperties.ValidationSchemaFactoryWrapper; +import com.fasterxml.jackson.module.jsonSchema.types.ArraySchema; +import com.fasterxml.jackson.module.jsonSchema.types.NumberSchema; +import com.fasterxml.jackson.module.jsonSchema.types.StringSchema; + +import javax.validation.constraints.*; +import java.util.List; +import java.util.Map; + +/** + * @author cponomaryov + */ +public class ValidationSchemaFactoryWrapperTest extends SchemaTestBase { + + public static class ValidationBean { + + /* + /********************************************************** + /* Array fields + /********************************************************** + */ + + private List listWithoutConstraints; + + @Size(min = 1) + private List listWithMinSize; + + @Size(max = 2) + private List listWithMaxSize; + + @Size(min = 3, max = 4) + private List listWithMinAndMaxSize; + + /* + /********************************************************** + /* Number fields + /********************************************************** + */ + + private int numberWithoutConstraints; + + @Min(5) + private int numberWithMin; + + @DecimalMin("5.5") + private int numberWithDecimalMin; + + @Max(6) + private int numberWithMax; + + @DecimalMax("6.5") + private int numberWithDecimalMax; + + @Min(7) + @Max(8) + private int numberWithMinAndMax; + + @Min(9) + @DecimalMax("9.5") + private int numberWithMinAndDecimalMax; + + @DecimalMin("10.5") + @Max(11) + private int numberWithDecimalMinAndMax; + + @DecimalMin("11.5") + @DecimalMax("12.5") + private int numberWithDecimalMinAndDecimalMax; + + /* + /********************************************************** + /* String fields + /********************************************************** + */ + + private String stringWithoutConstraints; + + @Size(min = 13) + private String stringWithMinSize; + + @Size(max = 14) + private String stringWithMaxSize; + + @Size(min = 15, max = 16) + private String stringWithMinAndMaxSize; + + public List getListWithoutConstraints() { + return listWithoutConstraints; + } + + public void setListWithoutConstraints(List listWithoutConstraints) { + this.listWithoutConstraints = listWithoutConstraints; + } + + public List getListWithMinSize() { + return listWithMinSize; + } + + public void setListWithMinSize(List listWithMinSize) { + this.listWithMinSize = listWithMinSize; + } + + public List getListWithMaxSize() { + return listWithMaxSize; + } + + public void setListWithMaxSize(List listWithMaxSize) { + this.listWithMaxSize = listWithMaxSize; + } + + public List getListWithMinAndMaxSize() { + return listWithMinAndMaxSize; + } + + public void setListWithMinAndMaxSize(List listWithMinAndMaxSize) { + this.listWithMinAndMaxSize = listWithMinAndMaxSize; + } + + public int getNumberWithoutConstraints() { + return numberWithoutConstraints; + } + + public void setNumberWithoutConstraints(int numberWithoutConstraints) { + this.numberWithoutConstraints = numberWithoutConstraints; + } + + public int getNumberWithMin() { + return numberWithMin; + } + + public void setNumberWithMin(int numberWithMin) { + this.numberWithMin = numberWithMin; + } + + public int getNumberWithDecimalMin() { + return numberWithDecimalMin; + } + + public void setNumberWithDecimalMin(int numberWithDecimalMin) { + this.numberWithDecimalMin = numberWithDecimalMin; + } + + public int getNumberWithMax() { + return numberWithMax; + } + + public void setNumberWithMax(int numberWithMax) { + this.numberWithMax = numberWithMax; + } + + public int getNumberWithDecimalMax() { + return numberWithDecimalMax; + } + + public void setNumberWithDecimalMax(int numberWithDecimalMax) { + this.numberWithDecimalMax = numberWithDecimalMax; + } + + public int getNumberWithMinAndMax() { + return numberWithMinAndMax; + } + + public void setNumberWithMinAndMax(int numberWithMinAndMax) { + this.numberWithMinAndMax = numberWithMinAndMax; + } + + public int getNumberWithMinAndDecimalMax() { + return numberWithMinAndDecimalMax; + } + + public void setNumberWithMinAndDecimalMax(int numberWithMinAndDecimalMax) { + this.numberWithMinAndDecimalMax = numberWithMinAndDecimalMax; + } + + public int getNumberWithDecimalMinAndMax() { + return numberWithDecimalMinAndMax; + } + + public void setNumberWithDecimalMinAndMax(int numberWithDecimalMinAndMax) { + this.numberWithDecimalMinAndMax = numberWithDecimalMinAndMax; + } + + public int getNumberWithDecimalMinAndDecimalMax() { + return numberWithDecimalMinAndDecimalMax; + } + + public void setNumberWithDecimalMinAndDecimalMax(int numberWithDecimalMinAndDecimalMax) { + this.numberWithDecimalMinAndDecimalMax = numberWithDecimalMinAndDecimalMax; + } + + public String getStringWithoutConstraints() { + return stringWithoutConstraints; + } + + public void setStringWithoutConstraints(String stringWithoutConstraints) { + this.stringWithoutConstraints = stringWithoutConstraints; + } + + public String getStringWithMinSize() { + return stringWithMinSize; + } + + public void setStringWithMinSize(String stringWithMinSize) { + this.stringWithMinSize = stringWithMinSize; + } + + public String getStringWithMaxSize() { + return stringWithMaxSize; + } + + public void setStringWithMaxSize(String stringWithMaxSize) { + this.stringWithMaxSize = stringWithMaxSize; + } + + public String getStringWithMinAndMaxSize() { + return stringWithMinAndMaxSize; + } + + public void setStringWithMinAndMaxSize(String stringWithMinAndMaxSize) { + this.stringWithMinAndMaxSize = stringWithMinAndMaxSize; + } + } + + /* + /********************************************************** + /* Unit tests, success + /********************************************************** + */ + + private final ObjectMapper MAPPER = new ObjectMapper(); + + private Object[][] listTestData() { + return new Object[][] {{"listWithoutConstraints", null, null}, + {"listWithMinSize", 1, null}, + {"listWithMaxSize", null, 2}, + {"listWithMinAndMaxSize", 3, 4}}; + } + + private Object[][] numberTestData() { + return new Object[][] {{"numberWithoutConstraints", null, null}, + {"numberWithMin", 5d, null}, + {"numberWithDecimalMin", 5.5, null}, + {"numberWithMax", null, 6d}, + {"numberWithDecimalMax", null, 6.5}, + {"numberWithMinAndMax", 7d, 8d}, + {"numberWithMinAndDecimalMax", 9d, 9.5}, + {"numberWithDecimalMinAndMax", 10.5, 11d}, + {"numberWithDecimalMinAndDecimalMax", 11.5, 12.5}}; + } + + private Object[][] stringTestData() { + return new Object[][] {{"stringWithoutConstraints", null, null}, + {"stringWithMinSize", 13, null}, + {"stringWithMaxSize", null, 14}, + {"stringWithMinAndMaxSize", 15, 16}}; + } + + /** + * Test set validation constraints + */ + @SuppressWarnings("SuspiciousMethodCalls") + public void testAddingValidationConstraints() throws Exception { + ValidationSchemaFactoryWrapper visitor = new ValidationSchemaFactoryWrapper(); + ObjectMapper mapper = new ObjectMapper(); + + mapper.acceptJsonFormatVisitor(ValidationBean.class, visitor); + JsonSchema jsonSchema = visitor.finalSchema(); + + assertNotNull("schema should not be null.", jsonSchema); + assertTrue("schema should be an objectSchema.", jsonSchema.isObjectSchema()); + Map properties = jsonSchema.asObjectSchema().getProperties(); + assertNotNull(properties); + for (Object[] testCase : listTestData()) { + JsonSchema propertySchema = properties.get(testCase[0]); + assertNotNull(propertySchema); + assertTrue(propertySchema.isArraySchema()); + ArraySchema arraySchema = propertySchema.asArraySchema(); + assertEquals(testCase[1], arraySchema.getMinItems()); + assertEquals(testCase[2], arraySchema.getMaxItems()); + } + for (Object[] testCase : numberTestData()) { + JsonSchema propertySchema = properties.get(testCase[0]); + assertNotNull(propertySchema); + assertTrue(propertySchema.isNumberSchema()); + NumberSchema numberSchema = propertySchema.asNumberSchema(); + assertEquals(testCase[1], numberSchema.getMinimum()); + assertEquals(testCase[2], numberSchema.getMaximum()); + } + for (Object[] testCase : stringTestData()) { + JsonSchema propertySchema = properties.get(testCase[0]); + assertNotNull(propertySchema); + assertTrue(propertySchema.isStringSchema()); + StringSchema stringSchema = propertySchema.asStringSchema(); + assertEquals(testCase[1], stringSchema.getMinLength()); + assertEquals(testCase[2], stringSchema.getMaxLength()); + } + } + +}