Matthew Lonis
Matthew Lonis

Reputation: 51

@Valid not working on nested objects (Java / Spring Boot)

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.

Upvotes: 5

Views: 10964

Answers (1)

geekTechnique
geekTechnique

Reputation: 890

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.

Upvotes: 2

Related Questions