@Valid JSON request with BindingResult causes IllegalStateException

18,495

Solution 1

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;
    }
}

Solution 2

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.

Solution 3

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.

Share:
18,495
finneycanhelp
Author by

finneycanhelp

I love people, creating solutions, and technology.

Updated on June 06, 2022

Comments

  • finneycanhelp
    finneycanhelp almost 2 years

    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.

  • finneycanhelp
    finneycanhelp over 12 years
    I look forward to trying that out! Thanks!
  • finneycanhelp
    finneycanhelp over 12 years
    We tried removing BindingResult and the validation did not work.
  • jbenckert
    jbenckert about 12 years
    We're using JSR303 and I'm having this problem with null values in the model class. I'd like to send the errors back to the view but handling them in the DefaultHandler fires the MethodArgumentNotValidException. I think it's too late for me to handle that gracefully at that point.
  • fashuser
    fashuser about 10 years
    What is @RequestParam(value = "data") in your post? I've tried to do the such think, but I've got exception that it is required attribute.
  • user3360944
    user3360944 over 9 years
    solution calls Validator explicitly and this should be done by Spring framework.
  • Grigory Kislin
    Grigory Kislin almost 8 years
    Here is the sample of MethodArgumentNotValidException treatment: dzone.com/articles/spring-31-valid-requestbody