kkflf
kkflf

Reputation: 2570

Bean validation in DropWizard with custom Json output

I have developing a webservice with a variety of Rest functions. I would like to use the standard @Valid annotation to validate my beans. I do however want to modify the the output json error.

Error messages from the validation is currently formatted like this:

{
  "errors": [
    "someString size must be between 0 and 140",
    "anotherString cannot contain numbers"
  ]
}

I do however want the error messages to be formatted like this:

{
    "errors": [{
            "someString": "size must be between 0 and 140"
        },
        {
            "anotherString": "cannot contain numbers"
        }
    ]
}

or

{
    "errors": [{
            "field": "someString"
            "error": "size must be between 0 and 140"
        },
        {
            "field": "anotherString"
            "error": "cannot contain numbers"
        }
    ]
}

I am aware of how I can alter the message for errors by either providing message="some message about strings" to the validating annotation and even use ValidationMEssages.properties as a common place for all error messages. I am however clueless of how I can change the output format if an error occours.

I have read the following documentation, but I need some more guidance. http://www.dropwizard.io/1.0.0/docs/manual/validation.html

This is my first DropWizard project, I am used to developing in Spring.

Thanks in advance.

Upvotes: 0

Views: 1617

Answers (1)

kkflf
kkflf

Reputation: 2570

I found the solution for my own problem. I have decided to post it if somebody should have the same problem as I did.

This is for DropWizard 1.0, I have not tested if it works on ealier/later versions, so keep that in mind. I cannot provide you will a complete solution, I have however posted my solution as code snippets, so do not expect that you can simply copy/paste and compile.

The solution is actually quiet simple, you just have to implement your own ExceptionMapper for ConstraintViolationException and reigster it with DropWizard.

You can easily specific your own messages for the bean validation by either providing a template or regular text. I.e.

@NotNull(message="God damn it, Morty. I ask you to do one thing!")

or

@NotNull(message="{morty.error}")

The templates are located in ValidationMessages.properties which you have to create yourself and place in src/main/resources/

ValidationMessages.properties

morty.error=God damn it, Morty. I ask you to do one thing!

Anyway, here is my solution.

SomeApplication.class

//Class where you setup DropWizard
public class SomeApplication extends Application<SomeConfiguration> {

    @Override
    public void run(SomeConfiguration conf, Environment environment) throws Exception {

        //Remove all default ExceptionMappers
        ((DefaultServerFactory)conf.getServerFactory()).setRegisterDefaultExceptionMappers(false);

        //Register your custom ExceptionMapper for ConstraintViolationException
        environment.jersey().register(new CustomExceptionMapper());

        //Restore the default ExceptionsMappers that you just removed
        environment.jersey().register(new LoggingExceptionMapper<Throwable>() {});
        environment.jersey().register(new JsonProcessingExceptionMapper());
        environment.jersey().register(new EarlyEofExceptionMapper());
    }
}

CustomExceptionMapper.class

//This is where the magic happens. 
//This is your custom ExceptionMapper for ConstraintViolationException
@Provider
public class CustomExceptionMapper implements ExceptionMapper<ConstraintViolationException> {

    @Override
    public Response toResponse(ConstraintViolationException cve) {
        //List to store all the exceptions that you whish to output
        //ValidationErrors is a custom object, which you can see further down
        ValidationErrors validationErrors = new ValidationErrors();

        //Loop through all the ConstraintViolations
        for(ConstraintViolation<?> c : cve.getConstraintViolations()){

            //We retrieve the variable name or method name where the annotation was called from.
            //This will be your property name for your JSON output.
            String field = ((PathImpl)c.getPropertyPath()).getLeafNode().getName();

            //If field is null, then the notation is probably at a class level.
            //Set field to class name
            if(field == null){
                field = c.getLeafBean().getClass().getSimpleName();
            }

            //c.getMessage() is the error message for your annotation.
            validationErrors.add(field, c.getMessage());
        }
        //Return a response with proper http status.
        return Response.status(422).entity(validationErrors).build();
    }
}

ValidationErrors

//There is not really any magic happening here.
//This class is just a wrapper for a List with the type ValidationObject.
public class ValidationErrors {

    public List<ValidationObject> errors = new ArrayList<ValidationObject>();

    public void add(String field, String error){
        errors.add(new ValidationObject(field, error));
    }
}

ValidationObject.class

//Once again, no magic
public class ValidationObject {

    //This will be your property names
    private String field, error;

    public ValidationObject(String field, String error){
        this.field = field;
        this.error = error;
    }

    public String getField() {
        return field;
    }

    public void setField(String field) {
        this.field = field;
    }

    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }
}

TestClass.class

//This is just a class to showcase the functionality
//I have not posted any codesnippers for this @CustomClassConstaint, 
//it is a custom annotaiton.
//I only included this annotation to show how  
//the CustomExceptionMapper handles annotations on a class level
@CustomClassConstaint 
public class TestClass {

    @NotNull
    @Size(min = 2, max = 5)
    public String testString1;

    @NotNull
    @Size(min = 2, max = 5)
    public String testString2;

    @Min(10)
    @Max(20)
    public int testInt1;

    @Min(10)
    @Max(20)
    public int testInt2;

}

Rest function for testing

    //Some rest function to showcase
    @POST
    @Path("/path/to/test")
    //Remember @Valid or annotations will not be validated
    public Response callRestTestMethod(@Valid TestClass testObject){
        return Response.ok().build();
    }

Input for test:

POST /path/to/test
{
  "testString1": null,
  "testString2": "",
  "testInt1": 9,
  "testInt2": 21
}

Output for test:

The order is somewhat random and changes everytime you call callRestTestMethod(...). The Validations are fired one by one in order they are in the component tree, I do not know if it is possible to controll the order.

{
  "errors": [
    {
      "field": "TestClass",
      "error": "custom error msg"
    },
    {
      "field": "testInt1",
      "error": "must be greater than or equal to 10"
    },
    {
      "field": "testString1",
      "error": "may not be null"
    },
    {
      "field": "testInt2",
      "error": "must be less than or equal to 20"
    },
    {
      "field": "testString2",
      "error": "size must be between 2 and 5"
    }
  ]
}

Upvotes: 3

Related Questions