13

When building a RESTful web service using Spring MVC, I feel I've encountered a tricky situation when trying to combine Jackson JSON deserialization and JSR-303 bean validation.

One of the neat things about JSR-303 validation is that one can return all validation errors associated with a target object rather than just fail on the first constraint violation (though there is a fail fast mode if you want to use it).

Imagine a scenario where you have a JSON payload such as:

{
   "firstname": "Joe",
   "surname": "Bloggs",
   "age": 25
}

And then you have the object you wish to map to:

public class Person {

   @NotNull
   private String firstname;

   @NotNull
   private String surname;

   @Min(value = 0)
   private int age;

   ...
}

My issue with this setup is that if the client sends a String as the value for "age", the whole Jackson deserialization process will fail without being able to specifically target the reason why it failed (i.e. we'll never get as far as validating). Imagine, then, this payload:

{
   "age": "invalid"
}

In this case, what I'd like to return would be an "errors" response such as:

{ 
   "errors" : [
      "error": {
         "field": "firstname",
         "message": "Must not be null",
      },
      "error": {
         "field": "surname",
         "message": "Must not be null",
      },
      "error": {
         "field": "age",
         "message": "Must be numeric value greater than or equal 0",
      }
   ]
}

The simple way around this problem would be to simply specify the "age" field as type "String" and then convert the value when passing it on to, say, the persistence layer. However, I don't really like the idea of resorting to data transfer objects that use Strings in a blanket fashion - it just seems wrong to do that in terms of preserving a clean design.

Another option I thought of would be to create a class such as:

public abstract class BindableValue<T> {

   private String stringValue;

   private T value;

   public T getValue() {
      if(value == null) {
         value = fromString(stringValue);
      }
      return value;
   }

   protected abstract T fromString(String stringValue);
}

I could then create implementations such as IntegerValue, LongValue, DateTimeValue etc. of course also writing custom type adapters for Jackson. And then my Person class could be:

public class Person {

   @NotNull
   private StringValue firstname;

   @NotNull
   private StringValue surname;

   @Min(value = 0)
   private IntegerValue age;

   ...
}

This is almost what I want but, unfortunately, the JSR-303 annotations such as @NotNull and @Min are not going to recognise my BindableValue implementations.

So, can anyone explain a way I could solve this problem in a cleaner way?

DrewEaster
  • 3,316
  • 3
  • 35
  • 39

3 Answers3

2

I realise now that the best way to do this may well be to simply use Strings for all fields but encapsulate that fact and still provide type specific method setters and getters. e.g.

public class Person {

    @NotNull
    private String name;

    @NotNull
    @Min(value = 0)
    private String age;

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age != null ? new Integer(age) : null;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(Integer age) {
        this.age = age == null ? null : String.valueOf(age);
    }

    @JsonSetter
    private void setAge(String age) {
        this.age = age;
    }
 }

In that way, I get the behaviour I'm after whilst ensuring the public object interface is still "clean".

DrewEaster
  • 3,316
  • 3
  • 35
  • 39
1

Another possibility is to just disable Jackson's complaints on unrecognized properties (see this wiki page), to "drop" unrecognized things, and validate things that did get bound. You can alternatively also define a listener for unrecognized properties. And then you could use Bean Validation for verifying data that did get bound.

StaxMan
  • 113,358
  • 34
  • 211
  • 239
0

Ok, since this was not yet mentioned.... DropWizard is the way to go I think -- it combines JAX-RS (Jersey) on Jackson with Bean Validation (hibernate impl I think), and it all "just works".

StaxMan
  • 113,358
  • 34
  • 211
  • 239
  • I made simple test and if you send json from first post of drewzilla with not a number value you get: { "code": 400, "message": "Unable to process JSON" }. So dropwizard doesn't resolve this problem. – wjtk Sep 16 '15 at 22:03
  • My comment was in relation to the post in its original form. DropWizard does not change semantics of Bean Validation, but it does solve the issue of how one enables its use. Other frameworks like Spring may provide this as well, at this point. But do note the time of original comment. – StaxMan Sep 22 '15 at 19:04