7

I've been trying for days to find a similar problem online and can't seem to find anything so I am asking my question here.

I have a controller:

import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Validated
@RestController
@RequestMapping("/data")
public class TheController {

    private final TheService theService;

    @Autowired
    public TheController(TheService theService) {
        this.theService = theService;
    }

    @PostMapping(path = "/data", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.TEXT_PLAIN_VALUE})
    public ResponseEntity<String> saveData(@Valid @RequestBody Data data) {
        subscriptionDataFeedService.sendData(data.getDataList());
        return ResponseEntity.ok()
                             .body("Data successful.");
    }
}

I have the request body class:

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Data {
    @NotEmpty(message = "Data list cannot be empty.")
    @JsonProperty(value = "dataArray")
    List<@Valid DataOne> dataList;
}

I have the DataOne class:

import com.fasterxml.jackson.annotation.JsonProperty;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DataOne {
    private @NotBlank String currency;
    private @NotBlank String accountNumber;
    private @NotBlank String finCode;
    private String poNumber;
    private @NotBlank String invoiceNumber;
    private @NotNull Address billTo;
    private @NotNull Address soldTo;
    private @NotNull LocalDate invoiceDate;
    private @NotBlank String billingPeriod;
    private @NotNull LocalDate paymentDueDate;
    private @NotNull BigDecimal amountDue;

    @JsonProperty(value = "activitySummary")
    private @NotNull List<@Valid ProductSummary> productSummaryList;

    @JsonProperty(value = "accountSummary")
    private @NotNull List<@Valid AccountSummary> accountSummaryList;

    @JsonProperty(value = "transactions")
    private @NotNull List<@Valid Transaction> transactionList;

    private @NotNull PaymentByACH paymentByACH;
    private @NotNull Address paymentByCheck;
    private @NotNull CustomerServiceContact customerServiceContact;
}

And I will include the Address class:

import javax.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Address {
    private @NotBlank String name;
    private @NotBlank String address1;
    private String address2;
    private @NotBlank String city;
    private @NotBlank String state;
    private @NotBlank String postalCode;
}

I omitted some of the other classes because they aren't needed for my question.

So the problem I am having is that the @Valid annotation is able to validate everything except for the nested classes inside DataOne that aren't a list. In other words, it cannot validate the fields inside Address, PaymentByACH, etc. However, it is able to validate that those objects are @NotNull but is unable to validate the fields inside those classes.

The @Valid is unable to validate the name, address 1, city, etc fields inside of Address. Whenever I add an @Valid tag in front of the Address field inside DataOne I get an HV000028: Unexpected exception during isValid call exception.

How can I validate the nested fields inside of the Address object or any of the nested objects?

TL;DR: The objects that are a list, such as List<@Valid Transaction> transactionList; does validate the fields inside of Transaction but the code does not validate the fields inside of Address.

3
  • Can you add the exception trace to the question? It may make it easier to get a good answer. Commented Nov 18, 2020 at 21:12
  • @Matthew Lonis - Hi, I see you're new to SO. If my answer helped you, please don't forget to mark this as answered. Commented Nov 19, 2020 at 1:25
  • 1
    You should annotate the dataList field with @Valid as well. If you want to validate nested complex objects or collections those need to be annotated. Currently you annotated the generic type of dataList and not the field. Hence it won't propagate. This ofcourse applies to the deeper nested fields as well. Also please remove the @Validated from the controller level you don't need it and will only complicate things. Commented Nov 19, 2020 at 6:20

1 Answer 1

3

Great question.

I think you're slightly misusing the @Valid annotation.

How can I validate the nested fields inside of the Address object or any of the nested objects?

@Valid shouldn't be prefixed to fields you want to validate. That tool is used specifically for validating arguments in @Controller endpoint methods (and sometimes @Service methods). According to docs.spring.io:

"Spring MVC has the ability to automatically validate @Controller inputs."

It offers the following example,

@Controller
public class MyController {
    @RequestMapping("/foo", method=RequestMethod.POST)
    public void processFoo(@Valid Foo foo) { /* ... */ }
}

The only reason you should use @Valid anywhere besides in the parameters of a controller (or service) method is to annotate complex types, like lists of objects (ie: DataOne: productSummaryList, accountSummaryList, transactionList). These docs have details for implementing your own validation policy if you'd like.

For your practical needs, you should probably only be using @Valid on controller level methods and the complex types for models referenced by that method. Then use field-level constraints to ensure you don't get things like negative age. For example:

@Data
...
public class Person {
    ...
    @Positive
    @Max(value = 117)
    private int age;
    ... 
}

Check out this list of constraints you can use from the spring docs. You're already using the @NotNull constraint, so this shouldn't be too foreign. You can validate emails, credit cards, dates, decimals, ranges, negative or positive values, and many other constraints.

Sign up to request clarification or add additional context in comments.

5 Comments

He isn't abusing anything. To validate complex nested objects and collections javax.validation requires those fields to be annotated with @Valid as well.
@M. Deinum it seems like you didn't even read my answer. You disagreed with me, then nearly quoted what I wrote. The only reason you should use @Valid anywhere besides in the parameters of a controller (or service) method is to annotate complex types
No i didn't. You state you shouldn't annotate fields in complex types nor that it shouldn't be done. If you want to propagate validation to fields which are themselves complex or collections you have to annotate those as well (you mention it should only be done on the lists). That is not what you are stating. If that is what you are meaning then your answer doesn't convey that.
I read it twice even checked your edits and you only state you should put them on the collections not on the fields inside Data which themselves are complex. So you state you shouldn't annotate the billTo field but only those of collections. However if you want validation to propagate that is exactly what you need to do as that is how javax.validation works.
...[use] @Valid on controller level methods and the complex types for models referenced by that method. I did not say you shouldn't annotate the "billTo" field, nor did I say anything about only annotating collections. You are misreading something.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.