diff --git a/src/main/java/org/unbrokendome/jackson/beanvalidation/BeanValidationModule.java b/src/main/java/org/unbrokendome/jackson/beanvalidation/BeanValidationModule.java index e893098..34c906f 100644 --- a/src/main/java/org/unbrokendome/jackson/beanvalidation/BeanValidationModule.java +++ b/src/main/java/org/unbrokendome/jackson/beanvalidation/BeanValidationModule.java @@ -1,10 +1,13 @@ package org.unbrokendome.jackson.beanvalidation; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler; +import javax.annotation.Nullable; import javax.validation.ConstraintViolationException; import javax.validation.ValidatorFactory; import java.io.IOException; @@ -15,6 +18,7 @@ public final class BeanValidationModule extends Module { private final ValidatorFactory validatorFactory; private final EnumSet features; + @Nullable private ConstructorValidatorFactory constructorValidatorFactory; public BeanValidationModule(ValidatorFactory validatorFactory) { @@ -56,13 +60,19 @@ public BeanValidationModule disable(BeanValidationFeature feature) { return this; } + public BeanValidationModule setConstructorValidatorFactory(@Nullable ConstructorValidatorFactory factory) { + this.constructorValidatorFactory = factory; + return this; + } + @Override public void setupModule(SetupContext context) { BeanValidationFeatureSet featureSet = new BeanValidationFeatureSet(features); - context.addBeanDeserializerModifier(new ValidationBeanDeserializerModifier(validatorFactory, featureSet)); + context.addBeanDeserializerModifier(new ValidationBeanDeserializerModifier( + validatorFactory, featureSet, constructorValidatorFactory)); context.addDeserializationProblemHandler(new DeserializationProblemHandler() { @Override @@ -75,6 +85,19 @@ public Object handleInstantiationProblem(DeserializationContext ctxt, Class i return super.handleInstantiationProblem(ctxt, instClass, argument, t); } } + + @Override + public boolean handleUnknownProperty( + DeserializationContext ctxt, JsonParser p, JsonDeserializer deserializer, + Object beanOrClass, String propertyName + ) throws IOException { + + if (beanOrClass instanceof InvalidObject) { + p.skipChildren(); + return true; + } + return super.handleUnknownProperty(ctxt, p, deserializer, beanOrClass, propertyName); + } }); } } diff --git a/src/main/java/org/unbrokendome/jackson/beanvalidation/ConstructorValidatorFactory.java b/src/main/java/org/unbrokendome/jackson/beanvalidation/ConstructorValidatorFactory.java new file mode 100644 index 0000000..e2e126e --- /dev/null +++ b/src/main/java/org/unbrokendome/jackson/beanvalidation/ConstructorValidatorFactory.java @@ -0,0 +1,9 @@ +package org.unbrokendome.jackson.beanvalidation; + +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.lang.reflect.Constructor; + +public interface ConstructorValidatorFactory { + Validator getValidator(ValidatorFactory validatorFactory, Constructor constructor, Object[] parameterValues); +} diff --git a/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidatingValueInstantiator.java b/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidatingValueInstantiator.java index bfd9631..43ba71d 100644 --- a/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidatingValueInstantiator.java +++ b/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidatingValueInstantiator.java @@ -16,6 +16,7 @@ import javax.validation.ConstraintViolationException; import javax.validation.ElementKind; import javax.validation.Path; +import javax.validation.Validator; import javax.validation.ValidatorFactory; import javax.validation.executable.ExecutableValidator; import java.io.IOException; @@ -32,16 +33,21 @@ class ValidatingValueInstantiator extends AbstractDelegatingValueInstantiator { private final ValidatorFactory validatorFactory; private final BeanValidationFeatureSet features; + private final ConstructorValidatorFactory constructorValidatorFactory; private boolean validationEnabled = false; ValidatingValueInstantiator( StdValueInstantiator delegate, ValidatorFactory validatorFactory, - BeanValidationFeatureSet features + BeanValidationFeatureSet features, + @Nullable ConstructorValidatorFactory constructorValidatorFactory ) { super(delegate); this.validatorFactory = validatorFactory; this.features = features; + this.constructorValidatorFactory = constructorValidatorFactory != null + ? constructorValidatorFactory + : (factory, constructor, parameterValues) -> factory.getValidator(); } @@ -182,11 +188,11 @@ private Set> validateCreatorArgs( } Member creatorMember = getWithArgsCreator().getMember(); - ExecutableValidator executableValidator = validatorFactory.getValidator().forExecutables(); if (creatorMember instanceof Constructor) { - return (Set) executableValidator.validateConstructorParameters( - (Constructor) creatorMember, args); + Constructor constructor = (Constructor) creatorMember; + Validator validator = constructorValidatorFactory.getValidator(validatorFactory, constructor, args); + return validator.forExecutables().validateConstructorParameters(constructor, args); } else if (creatorMember instanceof Method) { // Bean validation doesn't support parameter validation for static methods :-( diff --git a/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidationBeanDeserializerModifier.java b/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidationBeanDeserializerModifier.java index 98def8a..08ba275 100644 --- a/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidationBeanDeserializerModifier.java +++ b/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidationBeanDeserializerModifier.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.deser.impl.MethodProperty; import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator; +import javax.annotation.Nullable; import javax.validation.Validator; import javax.validation.ValidatorFactory; import java.util.ArrayList; @@ -18,11 +19,14 @@ final class ValidationBeanDeserializerModifier extends BeanDeserializerModifier private final ValidatorFactory validatorFactory; private final BeanValidationFeatureSet features; + private final ConstructorValidatorFactory constructorValidatorFactory; - ValidationBeanDeserializerModifier(ValidatorFactory validatorFactory, BeanValidationFeatureSet features) { + ValidationBeanDeserializerModifier(ValidatorFactory validatorFactory, BeanValidationFeatureSet features, + @Nullable ConstructorValidatorFactory constructorValidatorFactory) { this.validatorFactory = validatorFactory; this.features = features; + this.constructorValidatorFactory = constructorValidatorFactory; } @@ -49,7 +53,7 @@ public BeanDeserializerBuilder updateBuilder( ValueInstantiator valueInstantiator = builder.getValueInstantiator(); if (valueInstantiator instanceof StdValueInstantiator) { builder.setValueInstantiator(new ValidatingValueInstantiator( - (StdValueInstantiator) valueInstantiator, validatorFactory, features)); + (StdValueInstantiator) valueInstantiator, validatorFactory, features, constructorValidatorFactory)); } return builder; diff --git a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/AbstractValidationTest.kt b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/AbstractValidationTest.kt index e5908ae..51af843 100644 --- a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/AbstractValidationTest.kt +++ b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/AbstractValidationTest.kt @@ -9,7 +9,7 @@ import javax.validation.ConstraintViolationException import javax.validation.Validation -abstract class AbstractValidationTest { +abstract class AbstractValidationTest(constructorValidatorFactory: ConstructorValidatorFactory? = null) { private val validatorFactory = Validation.byDefaultProvider() .configure() @@ -17,6 +17,7 @@ abstract class AbstractValidationTest { .buildValidatorFactory() protected val beanValidationModule: BeanValidationModule = BeanValidationModule(validatorFactory) + .setConstructorValidatorFactory(constructorValidatorFactory) protected val objectMapper: ObjectMapper = ObjectMapper() .registerModule(beanValidationModule) diff --git a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/KotlinValidationTest.kt b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/KotlinValidationTest.kt index 9f64d3e..9c80fe6 100644 --- a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/KotlinValidationTest.kt +++ b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/KotlinValidationTest.kt @@ -144,7 +144,7 @@ class KotlinValidationTest : AbstractValidationTest() { private class NestedListArgumentsProvider : ArgumentsProvider { override fun provideArguments(context: ExtensionContext): Stream = Stream.of( - Arguments.of("nested[0].value", """{ "nested": [{}] }"""), + Arguments.of("nested[0].value", """{ "nested": [{}, {"value":"v1"}] }"""), Arguments.of("nested[1].value", """{ "nested": [{"value":"test"},{}] }"""), Arguments.of("nested[2].value", """{ "nested": [{"value":"1"},{"value":"2"},{}] }""") ) diff --git a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/HibernateConstructorValidatorFactory.kt b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/HibernateConstructorValidatorFactory.kt new file mode 100644 index 0000000..e0f155a --- /dev/null +++ b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/HibernateConstructorValidatorFactory.kt @@ -0,0 +1,19 @@ +package org.unbrokendome.jackson.beanvalidation.constraints + +import org.hibernate.validator.HibernateValidatorFactory +import org.unbrokendome.jackson.beanvalidation.ConstructorValidatorFactory +import java.lang.reflect.Constructor +import javax.validation.Validator +import javax.validation.ValidatorFactory + +class HibernateConstructorValidatorFactory : ConstructorValidatorFactory { + override fun getValidator(validatorFactory: ValidatorFactory, constructor: Constructor, parameterValues: Array): Validator { + val declaredFields = constructor.declaringClass.declaredFields + val payload: Map = parameterValues + .mapIndexed { index, value -> declaredFields[index].name to value} + .toMap() + + return validatorFactory.unwrap(HibernateValidatorFactory::class.java) + .usingContext().constraintValidatorPayload(payload).validator + } +} diff --git a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhen.kt b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhen.kt new file mode 100644 index 0000000..b9795f9 --- /dev/null +++ b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhen.kt @@ -0,0 +1,19 @@ +package org.unbrokendome.jackson.beanvalidation.constraints + +import javax.validation.Constraint +import javax.validation.Payload +import kotlin.annotation.AnnotationTarget.* +import kotlin.reflect.KClass + +@Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, FIELD, ANNOTATION_CLASS, CONSTRUCTOR, VALUE_PARAMETER) +@Retention +@MustBeDocumented +@Constraint(validatedBy = [NotNullWhenValidator::class]) +annotation class NotNullWhen( + val field: String, + val value: String, + val message: String = "{javax.validation.constraints.NotNull.message}", + val groups: Array> = [], + val payload: Array> = [] +) + diff --git a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidationTest.kt b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidationTest.kt new file mode 100644 index 0000000..389c940 --- /dev/null +++ b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidationTest.kt @@ -0,0 +1,59 @@ +package org.unbrokendome.jackson.beanvalidation.constraints + +import assertk.assertThat +import assertk.assertions.hasSize +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import org.junit.jupiter.api.Test +import org.unbrokendome.jackson.beanvalidation.AbstractValidationTest +import org.unbrokendome.jackson.beanvalidation.JsonValidated +import org.unbrokendome.jackson.beanvalidation.assertions.hasViolation + +class NotNullWhenValidationTest : AbstractValidationTest(HibernateConstructorValidatorFactory()) { + + @JsonValidated + class BeanWithCrossValidation + @JsonCreator constructor( + @param:JsonProperty("hasDetails") + val hasDetails: Boolean, + @param:JsonProperty("details") + @NotNullWhen(field = "hasDetails", value = "true") + val details: String? + ) + + @Test + fun `should not report violation when precondition is not matched`() { + + val json = """{ "hasDetails": false, "details": "" }""" + + assertNoViolationsOnDeserialization(json) + } + + + @Test + fun `should not report violation when precondition is matched and value is valid`() { + + val json = """{ "hasDetails": false, "details": "secure" }""" + + assertNoViolationsOnDeserialization(json) + } + + @Test + fun `should report violation when precondition is matched and value is null`() { + + val json = """{ "hasDetails": true }""" + + val violations = assertViolationsOnDeserialization(json) + + assertThat(violations).hasSize(1) + assertThat(violations).hasViolation("details") + } + + @Test + fun `should not report violation when precondition is matched and value is empty`() { + + val json = """{ "hasDetails": true, "details": "" }""" + + assertNoViolationsOnDeserialization(json) + } +} diff --git a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidator.kt b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidator.kt new file mode 100644 index 0000000..febf973 --- /dev/null +++ b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidator.kt @@ -0,0 +1,25 @@ +package org.unbrokendome.jackson.beanvalidation.constraints + +import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext +import java.util.* +import javax.validation.ConstraintValidator +import javax.validation.ConstraintValidatorContext + +class NotNullWhenValidator : ConstraintValidator { + private var field: String = "" + private var value: String = "" + + override fun initialize(notNullWhen: NotNullWhen) { + field = notNullWhen.field + value = notNullWhen.value + } + + override fun isValid(data: String?, context: ConstraintValidatorContext): Boolean { + val hibernateValidatorCtx = context.unwrap(HibernateConstraintValidatorContext::class.java) + val constructorParams = hibernateValidatorCtx.getConstraintValidatorPayload(Map::class.java) + val actualValue = constructorParams[field] + return actualValue == null || + !Objects.equals(actualValue.toString(), value) || + data != null + } +}