6

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?

Community
  • 1
  • 1
Richard
  • 5,840
  • 36
  • 123
  • 208
  • What you are looking for is called cross field validation. You can do this by creating a class level annotation and validating against it. [Here](http://stackoverflow.com/questions/1972933/cross-field-validation-with-hibernate-validator-jsr-303) is a very good example of the suggested implementation. – dambros Apr 01 '16 at 21:03
  • 1
    @dambros this seems to be only doable for Strings but I need to do it for other types like doubles, Lists, optionals...etc – Richard Apr 04 '16 at 00:23
  • Richard, check the edited answer please. – dambros Apr 04 '16 at 09:06
  • This isn't really the sort of validation the framework is intended to resolve. Bean Validation, as per JSR303, which is what the hibernate validator implements, is intended to validate that each field maps to the appropriate type. Cross field validation, of which there are many (this-or-this,this-and-this), is considered a business rule, not validation. So, although you can find answers to this question, they're likely to stretch the 'validation' concept beyond where it was intended to go. – Software Engineer Apr 04 '16 at 09:22

5 Answers5

9

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
}
Community
  • 1
  • 1
dambros
  • 4,252
  • 1
  • 23
  • 39
  • This worked perfectly! Auxiliary question, this throws an error that I catch but any idea how to get the request object in the exception handler? I tried using `HttpServletRequest` in the handler method parameter to get the request object but I can't figure out how to retrieve it – Richard Apr 04 '16 at 12:59
  • Check the edited answer for this. It is pretty simple – dambros Apr 04 '16 at 13:24
1

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...

Nathan
  • 1,576
  • 8
  • 18
1

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.

xsalefter
  • 640
  • 5
  • 16
0

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;
    }
}
Community
  • 1
  • 1
Steve Chambers
  • 37,270
  • 24
  • 156
  • 208
0

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

Community
  • 1
  • 1
uniknow
  • 938
  • 6
  • 5