Richard
Richard

Reputation: 6116

Java Hibernate Validator require one or another

This is an extension of my previous question. I implemented Dennis R's answer and am using hibernate-validator. Is there a way to require one field or another to be specified in the json request but not both? From my previous post, in the Request class I want the user to pass in either the id OR the code but NOT both.

I found this resource that might be the right solution for me but I don't fully understand what's going on there, why that works and frankly that looks entirely too verbose. Is that the only way to do it?

Upvotes: 6

Views: 5612

Answers (5)

xsalefter
xsalefter

Reputation: 640

You could use Group as an alternative. For example, this is the Request.java:

public class Request {

public interface IdOrCodeValidationGroup {}

    @NotNull
    @NotEmpty
    private String id;

    @Digits(integer=4, fraction=0)
    private double code;

    @NotNull
    @NotEmpty
    private String name;

    @AssertTrue(groups = IdOrCodeValidationGroup.class)
    private boolean idOrCodeFilled;

    public Request(String id, double code, String name) {
        this.id = id;
        this.code = code;
        this.name = name;
    }

    public boolean isIdOrCodeFilled() {
        if (id == null && code > 0) {
            idOrCodeFilled = true;
        } else if (id != null && code == 0) {
            idOrCodeFilled = true;
        } else idOrCodeFilled = false;
        return idOrCodeFilled;
    }
}

And then use validator like this:

@Test
public void testValidation() {
    // Of course all of this valid. No group at all.
    final Request request = new Request("ID-001", 111, "Data 1");
    final Set<ConstraintViolation<Request>> fails = this.validator.validate(request);
    Assert.assertTrue(fails.isEmpty());
}

@Test
public void testValidationWithGroup() {
    // We use "IdOrCodeValidationGroup.class" group, thus this is invalid.
    Request request = new Request("ID-001", 111, "Data 1");
    Set<ConstraintViolation<Request>> fails = this.validator.validate(request, IdOrCodeValidationGroup.class);
    Assert.assertFalse(fails.isEmpty());

    // Lets make one of constraint true; In this case, we set code = 0.
    request = new Request("ID-002", 0, "Data 2");
    fails = this.validator.validate(request, IdOrCodeValidationGroup.class);
    // Passed!
    Assert.assertFalse(fails.isEmpty()); 
}

Here is fully functional sample code. (Don't forget to checkout 'so-36365734' branch). And this is Official Documentation about Bean Validation Group.

HTH.

Upvotes: 1

uniknow
uniknow

Reputation: 938

You could use a JSON schema specifying the optional fields, and than validate the incoming JSON against the schema.

A clue on how the schema could look can be found in this answer json schema how do i require one field or another or one of two others but not all of them?

An approach on how to apply JSON schema validation can be found in this tutorial validate json against schema in java

Upvotes: 0

dambros
dambros

Reputation: 4392

As I commented earlier and following Nicko's answer from here, you can achieve what you want with the following code:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
public @interface FieldMatch {

    String message() default "something is wrong!";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * @return The first field
     */
    String first();

    /**
     * @return The second field
     */
    String second();

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        FieldMatch[] value();
    }

    public static class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {

        private String firstFieldName;
        private String secondFieldName;

        @Override
        public void initialize(FieldMatch fieldMatch) {
            firstFieldName = fieldMatch.first();
            secondFieldName = fieldMatch.second();
        }

        public boolean isValid(Object object, ConstraintValidatorContext constraintContext) {
            try {
                final Object firstObj = getProperty(object, firstFieldName);
                final Object secondObj = getProperty(object, secondFieldName);

                if(firstObj == null && secondObj == null || firstObj != null && secondObj != null) {
                    return false;
                }
            } catch (final Exception ignore) {
                // ignore
            }
            return true;
        }

        private Object getProperty(Object value, String fieldName) {
            Field[] fields = value.getClass().getDeclaredFields();
            for (Field field : fields) {
                if (field.getName().equals(fieldName)) {
                    field.setAccessible(true);
                    try {
                        return field.get(value);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
            return null;
        }
    }

}

Usage :

@FieldMatch.List({
        @FieldMatch(first = "name", second = "people"),
        @FieldMatch(first = "age", second = "abc")
})
public class Foo {

    private String name;
    private List<String> people;
    private int age;
    private Boolean abc; 
}

The only difference for you is that you don't want to check if the contents are equal, just if one field is null and the other isn't.

EDIT:

To get the object on your ExceptionHandler as asked via comments, you simply wrap the exception around a custom one and pass the object when you throw it, i.e.:

public class CustomException extends Exception {

    private String message;
    private Object model;

    public CustomException(String message, Object model) {
        super(message);
        this.model = model;
    }

    public Object getModel() {
        return model;
    }
}

With this, you can simply get it like this:

@ExceptionHandler(CustomException.class)
public ModelAndView handleCustomException(CustomException ex) {
    Object obj = ex.getModel();
    //do whatever you have to
}

Upvotes: 10

Steve Chambers
Steve Chambers

Reputation: 39414

First important tip: When using JSR-303 validation, your entities should always use wrapper types (e.g. Integer, Double etc.) rather than primitives (e.g. int, double etc.) for all fields - see here. The entity being validated can then be annotated with RequestCheck to mark it for custom validation:

@RequestCheck
public final class Request {
    //... Use Double, *not* double.
    private Double code;
    // ...
}

The RequestCheck annotation interface should look something like this:

/**
 * Request entity validator.
 */
@Target({ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = RequestValidator.class)
@Documented
public @interface ImpCheck {
    Class<?>[] groups() default {};
    String message() default "";
    Class<? extends Payload>[] payload() default {};
}

The above references a custom JSR-303 validator class:

/**
 * Validator for the Request entity.
 */
public class RequestValidator implements ConstraintValidator<RequestCheck, Request> {
    @Override
    public void initialize(final RequestCheck arg0) {
        // Required due to implementing ConstraintValidator but can be left blank.
    }

    @Override
    public boolean isValid(final Request request, final ConstraintValidatorContext ctx) {
        // Default validity is true until proven otherwise.
        boolean valid = true;

        // Disable default ConstraintViolation so a customised message can be set instead.
        ctx.disableDefaultConstraintViolation();

        // Either id or code (but not both) must be specified.
        // Note: ^ is Java's XOR operator, i.e. one or the other must be true but not both.
        if (!(request.getId() == null ^ request.getCode() == null)) {
            valid = false;
            ctx.buildConstraintViolationWithTemplate(
                "Request - either id or code (but not both) must be specified.")
                .addConstraintViolation();
        }

        return valid;
    }
}

Upvotes: 0

Nathan
Nathan

Reputation: 1661

My goodness. The linked references look to me to be unnecessarily complex. There exists an annotation:

@org.hibernate.annotations.Check

I have often had this same case, where I want to perform exactly this type of validation, I have one field or another, or I have both or neither...

@Entity
@org.hibernate.annotations.Check(constraints = "(field1 IS NULL OR field2 IS NULL) AND (field1 IS NOT NULL OR field2 IS NOT NULL)")
public class MyEntity{
    String field1;
    Double field2;
}

This will create a check-constraint in the DB which will enforce the constraint. It shifts the validation from Hibernate and your code to the DB (which will also prevent any applications that access your DB outside of your hibernate configuration from breaking this constraint).

The creation of this annotation does not automatically execute the creation of the constraint on your database, but if/when you create the constraint, it also informs hibernate about it.

In Postgres, this constraint looks like: ALTER TABLE my_entity ADD CONSTRAINT my_entity_check CHECK ((field1 IS NULL OR field2 IS NULL) AND (field1 IS NOT NULL OR field2 IS NOT NULL));

Postgres Check Constraints

Oracle Check Constraints

If you have trouble generating the exact SQL, create your annotation, and then allow hibernate to auto-generate your DB schema against an empty database, and it will show you the correct SQL. But with the annotation, hibernate knows about the constraint as well, so can be auto-generated if you allow hibernate to generate your schema for any automated tests, etc...

Upvotes: 2

Related Questions