java 133 lines · 4 tabs

Cascading @Valid on Nested DTOs with Validation Groups in Spring Boot

Shared by codesnips Aug 2026
4 tabs
package com.example.orders.dto;

import com.example.orders.validation.ValidationGroups.Create;
import com.example.orders.validation.ValidationGroups.Update;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.util.List;

public class OrderRequest {

    @Null(groups = Create.class, message = "id must be absent when creating")
    @NotNull(groups = Update.class, message = "id is required when updating")
    private Long id;

    @Valid
    @NotNull(message = "customer is required")
    private CustomerDto customer;

    @NotEmpty(message = "order must contain at least one line")
    private List<@Valid OrderLineDto> lines;

    public static class CustomerDto {
        @NotBlank(message = "name is required")
        private String name;

        @Email(message = "email must be valid")
        @NotBlank(message = "email is required")
        private String email;

        public String getName() { return name; }
        public String getEmail() { return email; }
    }

    public static class OrderLineDto {
        @NotBlank(message = "sku is required")
        private String sku;

        @Positive(message = "quantity must be greater than zero")
        private int quantity;

        @DecimalMin(value = "0.00", message = "unitPrice cannot be negative")
        private BigDecimal unitPrice;

        public String getSku() { return sku; }
        public int getQuantity() { return quantity; }
        public BigDecimal getUnitPrice() { return unitPrice; }
    }

    public Long getId() { return id; }
    public CustomerDto getCustomer() { return customer; }
    public List<OrderLineDto> getLines() { return lines; }
}
4 files · java Explain with highlit

This snippet shows how Spring Boot performs deep validation across nested request objects using cascading @Valid combined with Jakarta Bean Validation groups, so a single endpoint can enforce different rules for a create versus an update.

In OrderRequest DTO, the top-level object holds a CustomerDto and a list of OrderLineDto. The key detail is the @Valid annotation on those fields: without it, Bean Validation stops at the surface of OrderRequest and never descends into the nested customer or line items. Adding @Valid on customer and on the List<@Valid OrderLineDto> element tells the validator to cascade, walking the object graph and collecting violations from every level in one pass. Constraints are also assigned to groups such as Create and Update via the groups attribute, which lets the same field carry different requirements depending on the operation. For example, id is @Null on Create but @NotNull on Update.

The ValidationGroups marker file defines the empty marker interfaces used to select those groups. They extend Default where appropriate so that ungrouped constraints still run, avoiding the common pitfall where switching to a custom group silently skips every constraint that lacks an explicit group.

In OrderController, the create endpoint uses @Validated(Create.class) at the parameter to activate the create group, while the update path activates Update.class. Spring's @Validated is used here rather than plain @Valid precisely because only @Validated accepts a group argument; the nested @Valid annotations inside the DTO still drive the cascade. When validation fails, Spring throws MethodArgumentNotValidException before the handler body runs.

ApiExceptionHandler centralizes the response shape. It reads BindingResult from the exception, flattens each FieldError into a stable field-to-message map, and returns HTTP 422 so clients can bind messages to inputs. This keeps controllers thin and error contracts consistent. The trade-off of group-based validation is added ceremony and a subtle dependency on group inheritance; the benefit is one DTO and one validator serving multiple operations without duplicated request classes.


Related snips

graphql
type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
    createdAt: String!

GraphQL API with Spring Boot

java graphql spring-boot
by David Kumar 3 tabs
ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
java
package com.example.starter.config;

import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;

Custom Spring Boot starters

java spring-boot starter
by David Kumar 4 tabs
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form Validation Example</title>
  <style>

HTML forms with validation and accessibility

html forms validation
by Alex Chang 1 tab
java
package com.example.demo.config;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;

Messaging with Apache Kafka

java kafka messaging
by David Kumar 3 tabs
typescript
import axios from 'axios';

export type NormalizedErrors = {
  fields: Record<string, string>;
  formLevel: string | null;
};

Frontend: normalize and display server validation errors

ux typescript react
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Cascading @Valid on Nested DTOs with Validation Groups in Spring Boot — share card
Link copied