Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Java, how do I return meaningful, JSON formatted errors from Resteasy validation?

I have a RESTFul API consuming/returning JSON in the request/response body. When the client sends invalid data (valid JSON but invalid values for the fields) I want to be able to return a JSON structure (as well as the relevant 400+ code).

This structure would then allow the frontend to parse the errors on a per-field basis and render the errors alongside the input fields.

E.g. ideal output:

{
  "errors":{
    "name":["invalid chars","too long","etc"]
    "otherfield":["etc"]
  }
}

I am using Resteasy for the API, and using violation exceptions it's fairly easy to get it to render JSON errors:

@Provider
@Component
public class ValidationExceptionHandler implements ExceptionMapper<ResteasyViolationException> {
    public Response toResponse(ResteasyViolationException exception) {
        Multimap<String,String> errors = ArrayListMultimap.create();

        Consumer<ResteasyConstraintViolation> consumer = (violation) -> {
            errors.put(violation.getPath(), violation.getMessage());
        };

        exception.getParameterViolations().forEach(consumer);

        Map<String, Map<String, Collection<String>>> top = new HashMap<>();

        top.put("errors", errors.asMap());

        return Response.status(Status.BAD_REQUEST).entity(top)
                .build();
    }
}

However, the error paths (violation.getPath()) are property-centric rather than XmlElement-name-centric.

E.g. the above outputs:

{
  "errors":{"createCampaign.arg1.name":["invalid chars","etc"]}
}

I have tried stripping the index back from the last dot to get "name" but there are other issues with that hack.

E.g. if my "name" property isn't "name" it doesn't work:

@XmlElement(name="name")
@NotNull
private String somethingelse;

"somethingelse" will be returned to client, but they client has no idea what that is:

{
  "errors":{"somethingelse":["cannot be null"]}
}

The client wants "name" since that is what the field was called when they sent it.

My resource:

package com.foo.api;

import org.springframework.stereotype.Service;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import com.foo.dto.CarDTO;

@Service
@Path("/car")
public class CarResource {

    @POST
    @Produces({MediaType.APPLICATION_JSON})
    @Consumes(MediaType.APPLICATION_JSON)
    public CarDTO create(
            @Valid CarDTO car
    ) {
        //do some persistence
        return car;
    }
}

example dto:

package com.foo.dto;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
import javax.xml.bind.annotation.XmlElement;

public class CarDTO {
    @Min(1)
    @Max(10)
    @NotNull
    @XmlElement(name="gears")
    private int cogs;
}
like image 909
Sam Adams Avatar asked Dec 08 '14 17:12

Sam Adams


1 Answers

This article describes quite well what you need to do.

Basically you should implement an ExceptionMapper.

@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ValidationException> {

    @Override
    public Response toResponse(ValidationException exception) {
        Response myResponse;
        // build your Response based on the data provided by the exception
        return myResponse;
    }

}
like image 114
yamass Avatar answered Oct 10 '22 23:10

yamass