Jakarta Bean Validation & @Valid — declarative constraint trên DTO
Jakarta Bean Validation 3.0 (JSR 380): 23 built-in constraint, @Valid trigger và cascade nested object, @Valid vs @Validated — cơ chế AOP phía dưới, và tại sao declarative validation tách rule khỏi business logic.
TL;DR: Jakarta Bean Validation 3.0 (JSR 380) cung cấp 23 annotation constraint — @NotBlank, @Size, @Email, @Min/@Max, @Positive, @Past/@Future... Đặt annotation lên field DTO, sau đó thêm @Valid trên parameter method để Spring kích hoạt validation. Khi constraint fail, Spring ném MethodArgumentNotValidException và trả HTTP 400 trước khi method body chạy. @Valid cascade vào nested object; @Validated (Spring) bật method-level validation qua AOP proxy — khác với @Valid (Jakarta). Pitfall lớn nhất: Boot 3 tách spring-boot-starter-validation khỏi starter-web — quên add dependency thì annotation có nhưng validation silent skip.
Bài trước (Request binding) giải thích Spring map JSON body vào Java object như thế nào. Sau khi bind xong, câu hỏi tiếp theo là: dữ liệu vừa bind có hợp lệ không? Bài này trả lời đúng câu đó — cơ chế, annotation, và tại sao chọn declarative thay vì if/else manual.
1. Jakarta Bean Validation — chuẩn JSR 380
Jakarta Bean Validation 3.0 (trước đây là JSR 380, namespace javax.validation) là chuẩn Java EE định nghĩa API khai báo constraint lên field và method bằng annotation. Chuẩn này chỉ định nghĩa interface — implementation thật là Hibernate Validator, thư viện đi kèm spring-boot-starter-validation.
<!-- Boot 3+: PHAI add explicit, khong tu dong trong starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Dependency này kéo theo jakarta.validation-api (spec) và hibernate-validator (implementation). Thiếu nó — annotation compile được, nhưng constraint không trigger lúc runtime.
Vì sao declarative thay if/else?
Cách viết validation thủ công:
// Manual validation -- tron vao business logic
@PostMapping("/orders")
public OrderDto create(@RequestBody OrderRequest req) {
if (req.customer() == null || req.customer().isBlank()) {
throw new IllegalArgumentException("Customer required");
}
if (req.total() == null || req.total().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Total must be positive");
}
if (req.items() == null || req.items().isEmpty()) {
throw new IllegalArgumentException("Items required");
}
// ... 10 field nua, 30 dong if/else
return orderService.create(req);
}
Mỗi field thêm một if. Rule lặp lại qua mọi controller dùng cùng DTO. Khi rule thay đổi, phải tìm sửa ở nhiều chỗ.
Declarative:
public record OrderRequest(
@NotBlank String customer,
@NotNull @Positive BigDecimal total,
@NotEmpty @Valid List<OrderItem> items
) {}
@PostMapping("/orders")
public OrderDto create(@Valid @RequestBody OrderRequest req) {
// Khong mot dong if/else -- Spring validate truoc khi vao day
return orderService.create(req);
}
Rule sống trên DTO — một nơi duy nhất. Controller, service, test đều nhìn thấy cùng constraint. Đây là nguyên lý separation of concerns: rule validation tách khỏi logic xử lý.
flowchart LR
Client["Client<br/>POST /orders"] --> DS["DispatcherServlet"]
DS --> Resolver["ArgumentResolver<br/>bind JSON -> OrderRequest"]
Resolver --> Validator["Hibernate Validator<br/>check constraint"]
Validator -->|"fail"| Ex["MethodArgumentNotValidException<br/>-> 400 Bad Request"]
Validator -->|"pass"| Method["OrderController.create()<br/>business logic"]Validation xảy ra trước khi method body chạy — controller không bao giờ nhận object invalid.
2. 23 built-in constraint — bảng tra cứu
Jakarta Bean Validation 3.0 đi kèm 23 constraint chia theo nhóm:
Null / presence
| Annotation | Áp dụng | Ý nghĩa |
|---|---|---|
@NotNull | Object | Không được null |
@Null | Object | Phải null (dùng với validation group) |
@NotBlank | String | Không null, không empty, không toàn whitespace |
@NotEmpty | String, Collection, Map, Array | Không null, không empty |
@NotBlank mạnh hơn @NotEmpty: " " (chỉ space) fail @NotBlank nhưng pass @NotEmpty.
Number — range và sign
| Annotation | Ý nghĩa | Ví dụ |
|---|---|---|
@Min(value) | Số nguyên không nhỏ hơn value | @Min(1) |
@Max(value) | Số nguyên không lớn hơn value | @Max(100) |
@DecimalMin(value) | Số thực không nhỏ hơn value (BigDecimal precision) | @DecimalMin("0.01") |
@DecimalMax(value) | Số thực không lớn hơn value | @DecimalMax("999999.99") |
@Positive | Số dương, lớn hơn 0 | — |
@PositiveOrZero | Không âm, từ 0 trở lên | — |
@Negative | Số âm, nhỏ hơn 0 | — |
@NegativeOrZero | Không dương, từ 0 trở xuống | — |
@Digits(integer, fraction) | Tối đa N chữ số phần nguyên, M phần thập phân | @Digits(6, 2) |
String — độ dài và format
| Annotation | Ý nghĩa |
|---|---|
@Size(min, max) | Length của String, Collection, Array trong khoảng min-max |
@Email | Chuỗi hợp lệ theo format email |
@Pattern(regexp) | Match biểu thức chính quy Java |
Date / time
| Annotation | Ý nghĩa |
|---|---|
@Past | Thời điểm trong quá khứ (trước now) |
@PastOrPresent | Quá khứ hoặc hiện tại |
@Future | Thời điểm trong tương lai (sau now) |
@FutureOrPresent | Tương lai hoặc hiện tại |
Áp dụng cho LocalDate, LocalDateTime, Instant, Date, Calendar.
Boolean và cascade
| Annotation | Ý nghĩa |
|---|---|
@AssertTrue | boolean field hoặc method is*() phải trả true |
@AssertFalse | boolean field hoặc method phải trả false |
@Valid | Kích hoạt cascade — validate nested object |
Ví dụ kết hợp thực tế
public record OrderRequest(
@NotBlank(message = "Customer name is required")
@Size(min = 2, max = 100)
String customer,
@NotBlank
@Email
String email,
@NotNull
@Positive
@DecimalMax("999999.99")
BigDecimal total,
@NotNull
@Future(message = "Delivery date must be in the future")
LocalDate deliveryDate,
@NotEmpty
@Size(max = 100, message = "Maximum 100 items per order")
@Valid // cascade -- validate tung OrderItem
List<OrderItem> items,
@Pattern(regexp = "^[A-Z]{2}\\d{4}$", message = "Voucher format: 2 letters + 4 digits")
String voucherCode // nullable -- khong co @NotBlank
) {}
public record OrderItem(
@NotBlank String sku,
@Min(1) @Max(99) int quantity,
@NotNull @Positive BigDecimal unitPrice
) {}
Constraint gắn trên record component — Spring Hibernate Validator tự pick up khi @Valid kích hoạt.
3. Cơ chế bên dưới — @Valid trigger MethodArgumentNotValidException
@Valid trên method parameter không làm gì ở compile time. Tác dụng thật xảy ra lúc runtime, bên trong HandlerMethodArgumentResolver.
flowchart TB
Req["HTTP POST<br/>{\"customer\":\"\",\"total\":-5}"] --> DS["DispatcherServlet"]
DS --> RMAB["RequestResponseBodyMethodProcessor<br/>(ArgumentResolver cho @RequestBody)"]
RMAB --> Jackson["Jackson deserialize<br/>JSON -> OrderRequest object"]
Jackson --> Bind["DataBinder.validate()<br/>neu co @Valid tren parameter"]
Bind --> HV["Hibernate Validator<br/>doc constraint tren field"]
HV -->|"co violation"| MANE["MethodArgumentNotValidException<br/>chua BindingResult voi list FieldError"]
HV -->|"pass"| Controller["OrderController.create(req)<br/>business logic"]
MANE --> EH["@RestControllerAdvice<br/>handle -> 400 Problem Details"]Bước quan trọng là DataBinder.validate() — nó được gọi chỉ khi parameter có annotation @Valid hoặc @Validated. Không có annotation đó, validate() không được gọi, Hibernate Validator không chạy.
MethodArgumentNotValidException chứa BindingResult — danh sách FieldError, mỗi lỗi có:
field: tên field vi phạmcode: tên constraint (NotBlank,Size.min, ...)defaultMessage: message từ annotation
Bài Problem Details mổ xẻ cách build response 400 từ BindingResult thành format chuẩn RFC 9457.
@Valid cascade nested object
Nếu DTO chứa nested object, constraint bên trong nested chỉ được validate khi field đó có @Valid:
// Khong cascade -- constraint trong OrderItem bi skip
public record OrderRequest(
List<OrderItem> items
) {}
// Co cascade -- Hibernate Validator di vao tung OrderItem
public record OrderRequest(
@NotEmpty
@Valid List<OrderItem> items
) {}
Cascade hoạt động đệ quy: nếu OrderItem cũng chứa nested object có @Valid, Hibernate Validator tiếp tục đi sâu thêm.
4. @Valid vs @Validated — hai annotation khác gốc, khác mục đích
Đây là điểm gây nhầm lẫn phổ biến nhất:
| Aspect | @Valid (Jakarta) | @Validated (Spring) |
|---|---|---|
| Package | jakarta.validation | org.springframework.validation.annotation |
| Mục đích chính | Trigger validation trên parameter, cascade nested | Bật method-level validation qua AOP proxy |
| Vị trí dùng | Method parameter, field | Class, method, parameter |
| Hỗ trợ validation group | Không | Có — @Validated(Create.class) |
| Exception ném ra (controller) | MethodArgumentNotValidException | ConstraintViolationException |
@Validated class-level — bật method validation AOP
Đặt @Validated trên controller class để kích hoạt validation cho @RequestParam, @PathVariable, và parameter đơn lẻ:
@RestController
@Validated // BAT method-level validation
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping
public List<OrderDto> list(
@RequestParam @Min(0) int page,
@RequestParam @Min(1) @Max(100) int size,
@RequestParam @Email String filterEmail
) { ... }
}
Nếu không có @Validated trên class — @Min, @Max, @Email trên @RequestParam silently skip. Không có lỗi compile, không có warning — constraint âm thầm bị bỏ qua.
Cơ chế AOP phía dưới
Khi Spring thấy @Validated trên class, nó tạo một AOP proxy bao quanh class đó. Proxy intercept mọi method call, gọi MethodValidationInterceptor trước khi chuyển tiếp vào method thật:
flowchart LR
Caller["DispatcherServlet<br/>goi list(page=-1, ...)"] --> Proxy["OrderController Proxy<br/>(CGLIB-generated)"]
Proxy --> MVI["MethodValidationInterceptor<br/>@Min(0) tren page -- violation!"]
MVI -->|"fail"| CVE["ConstraintViolationException<br/>-> 400"]
MVI -->|"pass"| Real["OrderController.list()<br/>business logic"]ConstraintViolationException (từ @Validated AOP) khác MethodArgumentNotValidException (từ @Valid trên @RequestBody). Cả hai đều cần handler trong @RestControllerAdvice — bài Problem Details cover cách map cả hai thành cùng format 400.
@Valid vs @Validated — quy tắc thực tế
// Quy tac nho:
// @Valid --> dat tren @RequestBody hoac field de cascade
// @Validated --> dat tren CLASS de bat method validation (param, path var)
@RestController
@Validated // (1) bat method validation cho param/path
public class OrderController {
// (2) @Valid tren @RequestBody -- trigger + cascade
@PostMapping
public OrderDto create(@Valid @RequestBody OrderRequest req) { ... }
// (3) constraint tren param work nho @Validated class-level o tren
@GetMapping("/{id}")
public OrderDto findById(@PathVariable @NotNull @Positive Long id) { ... }
}
5. Pitfall thường gặp
Pitfall 1 — Quên @Valid trên @RequestBody
// SAI -- constraint tren OrderRequest bi skip hoan toan
@PostMapping("/orders")
public OrderDto create(@RequestBody OrderRequest req) { ... }
// DUNG
@PostMapping("/orders")
public OrderDto create(@Valid @RequestBody OrderRequest req) { ... }
Đây là pitfall số 1: annotation @NotBlank, @Size trên OrderRequest compile tốt nhưng không có hiệu lực. Invalid data đi thẳng vào service.
Pitfall 2 — Quên @Valid cascade nested object
// SAI -- OrderItem khong duoc validate du co @NotBlank @Min
public record OrderRequest(
@NotEmpty List<OrderItem> items // thieu @Valid
) {}
// DUNG
public record OrderRequest(
@NotEmpty @Valid List<OrderItem> items
) {}
Pitfall 3 — Quên @Validated trên controller class cho @RequestParam
// SAI -- @Min tren page silent skip
@RestController // thieu @Validated
public class OrderController {
@GetMapping
public List<OrderDto> list(@RequestParam @Min(0) int page) { ... }
}
// DUNG
@RestController
@Validated
public class OrderController {
@GetMapping
public List<OrderDto> list(@RequestParam @Min(0) int page) { ... }
}
Pitfall 4 — Thiếu dependency Boot 3
<!-- Boot 3 TACH ra -- phai add explicit -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Không add dependency thì annotation vẫn compile OK (API jar có sẵn trong classpath qua starter-web transitive), nhưng Hibernate Validator vắng mặt khiến ValidationAutoConfiguration không active — constraint không bao giờ trigger.
Cách kiểm tra nhanh:
mvn dependency:tree | grep hibernate-validator
# Phai thay: hibernate-validator-X.Y.Z.Final.jar
Ba pitfall đầu (thiếu @Valid, thiếu cascade, thiếu @Validated) và thiếu dependency đều gây ra cùng triệu chứng: invalid data đi qua mà không có lỗi. Không có exception, không có log warning. Cách phát hiện đáng tin cậy nhất: viết integration test gửi request invalid, assert response là 400.
Liên hệ các bài khác
- Request binding: bind JSON thành Java object là bước trước validation —
@Validchạy sau khi Jackson deserialize xong, trên object đã được bind. - Problem Details: khi
@Validtrigger và constraint fail, Spring némMethodArgumentNotValidException. Bài đó mổ xẻ cách@RestControllerAdvicebắt exception này và build response 400 theo RFC 9457 Problem Details — đây là nơiBindingResultđược đọc và format. - Validation advanced: bài tiếp theo đào sâu validation groups (
@Validated(Create.class)cho POST vs PUT khác rule), custom@Constraintannotation cho domain-specific rule, và i18n message quamessages.properties.
Tóm tắt
- Jakarta Bean Validation 3.0 (JSR 380) là chuẩn Java cho declarative constraint. Hibernate Validator là implementation, đi với
spring-boot-starter-validation— Boot 3 tách riêng, phải add explicit. - Declarative thắng manual: rule sống trên DTO — một nguồn sự thật. Không
if/elsescattered qua controller. - 23 built-in constraint nhóm theo: null/presence (
@NotNull,@NotBlank,@NotEmpty), number (@Min,@Max,@Positive,@Digits...), string (@Size,@Email,@Pattern), date (@Past,@Future...), boolean (@AssertTrue), cascade (@Valid). @Validtrigger validation: đặt trên@RequestBodyparameter khiếnDataBinder.validate()chạy; constraint fail némMethodArgumentNotValidExceptionvà client nhận 400 trước khi method body thực thi.@Validcascade: thêm@Validtrên field collection/nested object trong DTO để Hibernate Validator đi vào từng phần tử và validate constraint bên trong.@Validvs@Validated:@Valid(Jakarta) trigger/cascade;@Validated(Spring) class-level bật AOP proxy cho method-level validation — cần cho@RequestParam/@PathVariableconstraint.- Silent skip là bug class: thiếu
@Valid, thiếu cascade, thiếu@Validated, hoặc thiếu dependency đều cho kết quả giống nhau — invalid data qua mà không cảnh báo.
Tự kiểm tra
Q1Vì sao Jakarta Bean Validation chọn cách khai báo constraint bằng annotation thay vì yêu cầu dev viết logic validate bằng if/else trong mỗi method? Lợi ích thiết kế cụ thể là gì?▸
if/else trong mỗi method? Lợi ích thiết kế cụ thể là gì?Cách annotation (declarative) tách rule validation ra khỏi logic xử lý. Rule nằm trên DTO — một nguồn sự thật duy nhất. Mọi nơi dùng DTO (controller, service, test) đều nhìn thấy cùng constraint mà không cần copy.
Cách if/else (imperative) gây ra 3 vấn đề: (1) Rule bị scatter qua nhiều method — khi rule thay đổi phải tìm sửa nhiều chỗ. (2) Logic validate trộn với business logic — controller phải "biết" rule của domain. (3) Dễ bỏ sót: dev thêm controller mới copy-paste không đầy đủ.
Với annotation, container (Hibernate Validator qua AOP) intercept method call, validate constraint trước khi method body chạy. Dev không thể "quên" validate — chỉ có thể quên đặt @Valid (pitfall có thể catch bằng test).
Đây là biểu hiện của nguyên lý Separation of Concerns: rule validation là một concern tách biệt khỏi business logic, nên sống ở tầng khai báo (annotation trên DTO), không phải tầng thực thi (method body controller).
Q2Đoạn code sau có mấy vấn đề? Liệt kê đủ và giải thích mỗi vấn đề gây ra hậu quả gì.@RestController
@RequestMapping("/api/products")
public class ProductController {
@PostMapping
public ProductDto create(@RequestBody ProductRequest req) {
return service.create(req);
}
@GetMapping
public List<ProductDto> list(
@RequestParam @Min(0) int page,
@RequestParam @Min(1) @Max(50) int size
) {
return service.list(page, size);
}
}
public record ProductRequest(
@NotBlank String name,
@Positive BigDecimal price,
@NotEmpty @Valid List<CategoryRef> categories
) {}
public record CategoryRef(
@NotNull Long id,
@NotBlank String code
) {}
▸
@RestController
@RequestMapping("/api/products")
public class ProductController {
@PostMapping
public ProductDto create(@RequestBody ProductRequest req) {
return service.create(req);
}
@GetMapping
public List<ProductDto> list(
@RequestParam @Min(0) int page,
@RequestParam @Min(1) @Max(50) int size
) {
return service.list(page, size);
}
}
public record ProductRequest(
@NotBlank String name,
@Positive BigDecimal price,
@NotEmpty @Valid List<CategoryRef> categories
) {}
public record CategoryRef(
@NotNull Long id,
@NotBlank String code
) {}2 vấn đề:
Thiếu
@Validtrên@RequestBody:// SAI public ProductDto create(@RequestBody ProductRequest req) { ... } // DUNG public ProductDto create(@Valid @RequestBody ProductRequest req) { ... }Hậu quả: mọi constraint trên
ProductRequest(@NotBlank name,@Positive price,@NotEmpty categories) bị skip hoàn toàn. Request vớiname="",price=-1vẫn đi thẳng vàoservice.create(). Data invalid được persist vào DB. Bug này đặc biệt nguy hiểm vì không có exception, không có log — silent failure.Thiếu
@Validatedtrên class controller:// SAI @RestController @RequestMapping("/api/products") public class ProductController { ... } // DUNG @RestController @Validated @RequestMapping("/api/products") public class ProductController { ... }Hậu quả: constraint trên
@RequestParam—@Min(0) int page,@Min(1) @Max(50) int size— silently skip. Request vớipage=-999hoặcsize=9999qua mà không có lỗi 400. Pagination logic nhận giá trị tùy ý, có thể gây query chậm hoặc out-of-memory nếusizerất lớn.
Lưu ý: constraint cascade @Valid List<CategoryRef> trên ProductRequest khai báo đúng — nếu vấn đề (1) được sửa, cascade sẽ hoạt động và validate từng CategoryRef.
Code đúng:
@RestController
@Validated
@RequestMapping("/api/products")
public class ProductController {
@PostMapping
public ProductDto create(@Valid @RequestBody ProductRequest req) { ... }
@GetMapping
public List<ProductDto> list(
@RequestParam @Min(0) int page,
@RequestParam @Min(1) @Max(50) int size
) { ... }
}Q3Giải thích chính xác điều gì xảy ra bên dưới khi Spring xử lý request POST /orders với body JSON invalid — từ lúc request đến khi trả response 400. Nhắc đến đúng class Java tham gia.▸
POST /orders với body JSON invalid — từ lúc request đến khi trả response 400. Nhắc đến đúng class Java tham gia.Luồng cụ thể theo thứ tự:
DispatcherServletnhận request, tìm handler method khớp vớiPOST /orders.RequestResponseBodyMethodProcessor(mộtHandlerMethodArgumentResolver) được chọn để resolve parameter@Valid @RequestBody OrderRequest req. Jackson deserialize JSON body thànhOrderRequestobject.RequestResponseBodyMethodProcessorphát hiện annotation@Validtrên parameter — gọiDataBinder.validate()vớiOrderRequestobject vừa bind.- Hibernate Validator (implementation của Jakarta Bean Validation) đọc annotation constraint trên field, check từng field. Với JSON invalid, có ít nhất một constraint bị vi phạm.
- Hibernate Validator trả danh sách
ConstraintViolation.DataBindergom thànhBindingResultchứa listFieldError. RequestResponseBodyMethodProcessorthấyBindingResultcó error nên némMethodArgumentNotValidException(chứaBindingResultbên trong).- Spring tìm
@ExceptionHandler(MethodArgumentNotValidException.class)trong@RestControllerAdvice. Handler đọcex.getBindingResult().getFieldErrors(), buildProblemDetail400, trả response JSON.
Method body OrderController.create() không bao giờ được gọi — exception ném tại bước 6, trước khi Spring forward vào method.
Q4Sự khác biệt thực tế giữa @Valid (Jakarta) và @Validated (Spring) là gì? Khi nào dùng cái nào?▸
@Valid (Jakarta) và @Validated (Spring) là gì? Khi nào dùng cái nào?Khác nhau về nguồn gốc và cơ chế:
@Valid là annotation của Jakarta Bean Validation (spec chuẩn). Tác dụng: trigger DataBinder.validate() trên parameter, hoặc đánh dấu field cần cascade. Không hỗ trợ validation group.
@Validated là annotation của Spring Framework. Đặt ở class-level để Spring tạo AOP proxy (CGLIB) bao quanh class. Proxy intercept mọi method call, gọi MethodValidationInterceptor trước khi chuyển tiếp. Hỗ trợ validation group — @Validated(Create.class).
Exception khác nhau: @Valid trên @RequestBody → MethodArgumentNotValidException; @Validated AOP → ConstraintViolationException. Cần handler riêng cho mỗi loại trong @RestControllerAdvice.
Quy tắc thực tế:
@Validtrên@RequestBodyparameter — trigger validation cho JSON body.@Validtrên field trong DTO — cascade vào nested object.@Validatedtrên controller class — bật method-level validation, cần cho constraint trên@RequestParam/@PathVariable.@Validated(Group.class)trên parameter — khi cần validation group (POST vs PUT rule khác nhau).
Kết quả: nhiều code dùng cả hai cùng lúc — @Validated trên class để bật infrastructure, @Valid trên parameter để trigger/cascade.
Q5DTO ShipmentRequest dưới có field packages là list nested object. Constraint nào trên PackageItem sẽ được validate và constraint nào bị skip? Tại sao? Sửa để validate đúng.public record ShipmentRequest(
@NotBlank String trackingNumber,
@Future LocalDate estimatedArrival,
@Size(min = 1, max = 50) List<PackageItem> packages
) {}
public record PackageItem(
@NotBlank String description,
@Positive double weightKg,
@Min(1) int quantity
) {}
▸
ShipmentRequest dưới có field packages là list nested object. Constraint nào trên PackageItem sẽ được validate và constraint nào bị skip? Tại sao? Sửa để validate đúng.public record ShipmentRequest(
@NotBlank String trackingNumber,
@Future LocalDate estimatedArrival,
@Size(min = 1, max = 50) List<PackageItem> packages
) {}
public record PackageItem(
@NotBlank String description,
@Positive double weightKg,
@Min(1) int quantity
) {}Câu trả lời: Không constraint nào trên PackageItem được validate.
Dù ShipmentRequest có @Size(min=1, max=50) trên packages — constraint này chỉ check kích thước của list (số phần tử từ 1 đến 50). Nó không đi vào từng PackageItem để check constraint bên trong.
Để Hibernate Validator cascade vào từng phần tử của list và validate @NotBlank description, @Positive weightKg, @Min(1) quantity, cần thêm @Valid trên field packages:
public record ShipmentRequest(
@NotBlank String trackingNumber,
@Future LocalDate estimatedArrival,
@Size(min = 1, max = 50)
@Valid // them @Valid de cascade
List<PackageItem> packages
) {}Với @Valid thêm vào, Hibernate Validator sẽ: (1) check @Size(min=1,max=50) trên list — đúng số phần tử, và (2) với mỗi phần tử PackageItem trong list, check @NotBlank, @Positive, @Min(1).
Hậu quả của thiếu @Valid: list có thể chứa package với description="", weightKg=-5 — qua validation mà không bị bắt. Data corrupt đi vào DB hoặc downstream service.
Nguyên tắc nhớ: @Size/@NotEmpty trên collection chỉ check collection itself (length). @Valid thêm vào mới activate cascade vào từng element.
Bài tiếp theo: Validation groups, custom constraint & i18n
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên