How to use javax.validation and JSON request in Spring 4 MVC?

10,259

Solution 1

SOLUTION (Updated by questioner: Jessai)

I checked this question: Spring MVC 400 Bad Request Ajax.

In summary what I did:

  1. Create an object to be parsed with JSON.stringify and send it to the controller.

  2. In the controller I set the method with @ResponseBody and @RequestBody as @James Massey said.

  3. In the entity I added @JSONProperty (I had these already) and @JSONIgnore (I added to cheId field) annotations to the fields.

Javascript:

    var ssiCheque = {
            cheNumero : $("#formAddChecks #cheNumero").val(),
            cheRecepto : $("#formAddChecks #cheReceptor").val(),
            cheMonto : $("#formAddChecks #cheMonto").val(),
            cheFecha : $("#formAddChecks #cheFecha").val(),
            cheConcepto : $("#formAddChecks #cheConcepto").val()
    };


    $.ajax({
        type: "POST",
        contentType: "application/json",
        url: "addCheck",
        data: JSON.stringify(ssiCheque),
        dataType: "json",
        beforeSend: function ( xhr ) {
            console.log("before Send");
        },
        error: function (request, status, error) {            
            console.log('Error ' /*+ request.responseText*/ + "\n" + status + "\n" + error);
        },
        success: function(data) {
            console.log(data);
        }
    });

Controller

@RequestMapping(value = "/addCheck", method = RequestMethod.POST)
@ResponseBody
public SsiCheque addChecks(@Valid @RequestBody SsiCheque ssiCheque, BindingResult result) {

    //ssiCheque.persist();
    System.out.println("agregar " + result.getErrorCount());
    return ssiCheque;
}

Thanks!

Solution 2

I have solved my validations in another way. Suppose I have and Agent Object:

public class Agent {
    public int userID;
    public String name;
    public boolean isVoiceRecorded;
    public boolean isScreenRecorded;
    public boolean isOnCall;
}

I would like to validate : (1) userID>0 (2) name is mandatory (3) isVoiceRecorded and isScreenRecorded can be true only if isOnCall is true.

In order to do so I need to add dependency :

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
</dependency>

Now look how Agents class looks like:

@NoArgsConstructor
@ToString
@EqualsAndHashCode(of = "userID")
@CheckBools
public class Agent {
    @Min(0)
    public int userID;
    @NotNull(message = "Name cannot be null")
    public String name;
    public boolean isVoiceRecorded;
    public boolean isScreenRecorded;
    public boolean isOnCall;
    public LocalDateTime startEventDateTime;
}

(1) @Min(0) - solves userID>0 (2) @NotNull(message = "Name cannot be null") - solves name is mandatory, and you have example how to specify error message (3) @CheckBools annotation defined by me, at the class level which checks isVoiceRecorded and isScreenRecorded can be true only if isOnCall is true.

@Documented
@Constraint(validatedBy = MyConstraintValidator.class)
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
public @interface CheckBools {
    String message() default "'isVoiceRecorded' or 'isScreenRecorded' can be true only if you are on call";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

In the following class you define the rule

public class MyConstraintValidator implements ConstraintValidator<CheckBools, Agent> {
     @Override
    public void initialize(CheckBools constraintAnnotation) {

    }

    @Override
    public boolean isValid(Agent value, ConstraintValidatorContext context) {
        if (!value.isOnCall && (value.isVoiceRecorded || value.isScreenRecorded))
            return false;
        else return true;
    }
}

At the controller level :

@RestController
@RequestMapping("Myteamview")
public class MyteamviewController {
    @Autowired
    AgentInfo agentInfo;

    @RequestMapping(path = "agents", method = RequestMethod.POST)
    public ResponseEntity<Boolean> addOrUpdateAgent(@Valid @RequestBody Agent agent) {
        ResponseEntity<Boolean> responseEntity = new ResponseEntity<>(agentInfo.addAgent(agent),HttpStatus.OK);
        return responseEntity;
    }
}

Note: The important is that you specify @Valid before @RequestBody Agent

Solution 3

There appear to be a few problems here:

  1. Your object structure seems weird. Why are your fields referencing an object type? private Date SsiCheque.cheFecha seems to be a totally non-sensical field.

  2. You generally design your UI to send through a JSON object that can be mapped directly into your Java object. So if your object looked like this:

    public class Example { 
        @NotNull
        @Digits(fraction = 2, integer = 10)
        private Integer foo;
        @NotEmpty
        private String bar;
        @NotEmpty
        private String[] baz;
    }
    

    Then your JSON structure would be something like this:

    {
        "example": {
            "foo": 1,
            "bar": "Pineapple",
            "baz": [
                "This is a string",
                "So is this"
            ]
        }
    }
    

Which can be used by Jackson to map straight into your object.

You would then write your controller method like this assuming that you had the Jackson JAR included in your project classpath:

@RequestMapping(value = "/example", method = RequestMethod.POST)
@ResponseBody
public Example(@Valid @RequestBody Example example, BindingResult result) {
     if(result.hasErrors()){
         //A validation has failed, return an error response to the UI
     } else {
         exampleService.createOrUpdate(example);
         return example;
     }
}

The important part is that your object is the request body and you use the @RequestBody annotation, as Jackson uses this as a signal to construct your object using the JSON present in your HTTP Request Body. The only downside to this method is that you may have to construct your request JSON programmatically. This is trivial to do with JavaScript however. (I'm going to assume some sensible input id defaults here, and that you are familiar with the jQuery DOM manipulation/selection syntax)

var bazArray = [];
$.forEach($("#bazContainer"), function (baz, i){
    bazArray.push(baz);
});
var example = {
    foo: $("#fooInput").val(),
    bar: $("#barInput").val(),
    baz: bazArray
};

You pass in your example object to your request in the data field, and if you specify that it is of type application/json then jQuery will automatically call JSON.stringify on your example object. Hopefully this all makes sense.

Share:
10,259
Jessai
Author by

Jessai

I want to contribute and learn with the community of SO

Updated on June 15, 2022

Comments

  • Jessai
    Jessai almost 2 years

    I'm developing a Web App using Spring 4 MVC. I want to know If I can validate JSON request objects with javax.validation API. For example I have this chunk of my entity code:

        ...       
        @JsonProperty("cheFecha")
        @NotNull
        @Column(name = "che_fecha")
        @Temporal(TemporalType.DATE)
        @DateTimeFormat(style = "M-")
        private Date SsiCheque.cheFecha;
    
        @JsonProperty("cheMonto")
        @NotNull
        @JsonSerialize(using = CurrencySerializer.class)
        @Column(name = "che_monto", precision = 10, scale = 2)
        private BigDecimal SsiCheque.cheMonto;
        ...
    

    I have the controller code:

    @RequestMapping(value = "/addCheck", method = RequestMethod.POST)
    public @ResponseBody SsiCheque addChecks(@Valid SsiCheque ssiCheque, BindingResult result) {
    
        //ssiCheque.persist();
        System.out.println("add" + result.getErrorCount());// Zero when there are errors
        return ssiCheque;
    }
    

    And finally I have the jQuery code:

        var formData = $("#formAddChecks :input").serializeArray();
        $.ajax({
            type: "POST",
            url: "addCheck",
            data: formData,
            beforeSend: function ( xhr ) {
                console.log("before Send");
            },
            error: function (request, status, error) {            
                console.log('Error ' + "\n" + status + "\n" + error);
            },
            success: function(data) {
                console.log(data);
            }
        });
    

    The JSON object is arriving correctly to the controller but I want to validate the JSON with the entity javax.annotations API. What I have seen is only using custom validators and "rewrite" the validation code.

    Is this the only way to validate JSON?

    Thanks in advance!

    UPDATE 1

    I followed the @James Massey suggestions and my code looks like this right now:

    Controller

    @RequestMapping(value = "/addCheck", method = RequestMethod.POST)
    @ResponseBody
    public SsiCheque addChecks(@Valid @RequestBody SsiCheque ssiCheque, BindingResult result) {
    
        //ssiCheque.persist();
        System.out.println("agregar " + result.getErrorCount());
        return ssiCheque;
    }
    

    Javascript file

        var ssiCheque = {
                cheNumero : $("#formAddChecks cheNumero").val(),
                cheRecepto : $("#formAddChecks cheReceptor").val(),
                cheMonto : $("#formAddChecks cheMonto").val(),
                cheFecha : $("#formAddChecks cheFecha").val(),
                cheConcepto : $("#formAddChecks cheConcepto").val()
        };
    
    
        $.ajax({
            type: "POST",
            contentType: "application/json",
            url: "addCheck",
            data: ssiCheque,
            dataType: "json",
            beforeSend: function ( xhr ) {
                console.log("before Send");
            },
            error: function (request, status, error) {            
                console.log('Error ' /*+ request.responseText*/ + "\n" + status + "\n" + error);
            },
            success: function(data) {
                console.log(data);
            }
        });
    

    But I'm getting an 400 Error (Incorrect request) when I submit the form and execute the Ajax function. I have faced this error before when the json object format and the controller specs were incompatible, but in this time I don't know why can be the error.

    Thanks again!