Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

@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.

like image 219
Matthew Lonis Avatar asked Aug 11 '26 13:08

Matthew Lonis


1 Answers

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.

like image 101
geekTechnique Avatar answered Aug 14 '26 08:08

geekTechnique



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!