Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

@Valid JSON request with BindingResult causes IllegalStateException

I have a REST service which takes a JSON request. I want to validate the JSON request values that are coming in. How can I do that?

In Spring 3.1.0 RELEASE, I know one wants to make sure they are using the latest support classes listed at 3.1.13 New HandlerMethod-based Support Classes For Annotated Controller Processing

The old ones are items like: AnnotationMethodHandlerAdapter. I want to make sure I am using the latest such as RequestMappingHandlerAdapter.

This is because I hope it fixes an issue where I see this:

java.lang.IllegalStateException: Errors/BindingResult argument declared without preceding model attribute. Check your handler method signature!

My @Controller handler method and associated code is this:

@Autowired FooValidator fooValidator;

@RequestMapping(value="/somepath/foo", method=RequestMethod.POST)
public @ResponseBody Map<String, String> fooBar(
        @Valid @RequestBody Map<String, String> specificRequest,
        BindingResult results) {

    out("fooBar called");

    // get vin from JSON (reportRequest)

    return null;
}


@InitBinder("specificRequest") // possible to leave off for global behavior
protected void initBinder(WebDataBinder binder){
    binder.setValidator(fooValidator);
}

FooValidator looks like this:

@Component
public class FooValidator  implements Validator {

    public boolean supports(Class<?> clazz) {
        out("supports called ");
        return Map.class.equals(clazz);
    }

    public void validate(Object target, Errors errors) {
        out("validate called ");
    }


    private void out(String msg) {
        System.out.println("****** " + getClass().getName() + ": " + msg);
    }
}

If I remove the BindingResult, everything works fine except I won't be able to tell if the JSON validated.

I am not strongly attached to the concept of using a Map<String, String> for the JSON request or using a separate validator as opposed to a Custom Bean with validation annotation (How do you do that for a JSON request?). Whatever can validate the JSON request.

like image 538
finneycanhelp Avatar asked Jan 25 '12 13:01

finneycanhelp


3 Answers

3.1.17 @Valid On @RequestBody Controller Method Arguments says that:

An @RequestBody method argument can be annotated with @Valid to invoke automatic validation similar to the support for @ModelAttribute method arguments. A resulting MethodArgumentNotValidException is handled in the DefaultHandlerExceptionResolver and results in a 400 response code.

In other words, if you use @Valid @RequestBody then Spring will reject an invalid request before it gets as far as calling your method. if you method is invoked, then you can assume the request body is valid.

BindingResult is used for validation of form/command objects, rather than @RequestBody.

like image 81
skaffman Avatar answered Nov 09 '22 10:11

skaffman


I had to do something similar once. I just ended up making my life simpler by creating a Java object that the JSON could be convert into and used GSON to do the conversion.

It was honestly as simple as:

@Autowired
private Gson gson;

@RequestMapping(value = "/path/info", method = RequestMethod.POST)
public String myMethod(@RequestParam(value = "data") String data,
                       Model model,
                       @Valid MyCustomObject myObj,
                       BindingResult result) {
    //myObj does not contain any validation information.
    //we are just using it as as bean to take advantage of the spring mvc framework.
    //data contains the json string.
    myObj = gson.fromJson(data, MyCustomObject.class);

    //validate the object any way you want. 
    //Simplest approach would be to create your own custom validator 
    //to do this in Spring or even simpler would be just to do it manually here.
    new MyCustomObjValidator().validate(myObj, result);

    if (result.hasErrors()) {
        return myErrorView;
    }
    return mySuccessView;
}

Do all your validation in your custom Validator class:

public class MyCustomObjValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return MyCustomObj.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        MyCustomObj c = (MyCustomObj) target;
        Date startDate = c.getStartDate();
        Date endDate = c.getEndDate();
        if (startDate == null) {
            errors.rejectValue("startDate", "validation.required");
        }
        if (endDate == null) {
            errors.rejectValue("endDate", "validation.required");
        }
        if(startDate != null && endDate != null && endDate.before(startDate)){
            errors.rejectValue("endDate", "validation.notbefore.startdate");
        }
    }

}

MyCustomObject does not contain any annotation for validation, this is because otherwise Spring will try to validate this fields in this object which are currently empty because all the data is in the JSON String, it could for example be:

public class MyCustomObject implements Serializable {
    private Date startDate;
    private Date endDate;

    public Date getStartDate() {
        return startDate;
    }

    public Date getEndDate() {
        return endDate;
    }

    public void setStartDate(Date theDate) {
        this.startDate = theDate;
    }

    public void setEndDate(Date theDate) {
        this.endDate = theDate;
    }
}
like image 30
Ali Avatar answered Nov 09 '22 10:11

Ali


Try using the following:

@Autowired
private FooValidator fooValidator;

@InitBinder("specificRequest") // possible to leave off for global behavior
protected void initBinder(WebDataBinder binder){
    binder.setValidator(fooValidator);
}
@ModelAttribute("specificRequest")
public Map<String, String> getModel() {
    return new HashMap<String, String>();
}

This will make your controller serialize the request into the type you specify it to be. I have to say i normally dont make a service (autowired) of the validator, but it might be better.

Your handler looks like this now:

@RequestMapping(value="/somepath/foo", method=RequestMethod.POST)
public @ResponseBody Map<String, String> fooBar(
    @Valid @ModelAttribute("specificRequest") 
    Map<String, String> specificRequest, BindingResult results) {

    out("fooBar called");

    // get vin from JSON (reportRequest)

    return null;
}

To my knowledge this works perfectly and addresses the error you are receiving.

like image 1
fonZ Avatar answered Nov 09 '22 09:11

fonZ