最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

java - Should I use Value Objects or Bean Validation for DTOs in Sprint Boot? - Stack Overflow

programmeradmin5浏览0评论

I have the two following DTOs:

public record BankCreateRequest(
      @NotBlank(message = "The name is a required field.")
      @Length(max = 255, message = "The name cannot be longer than 255 characters.")
      @Pattern(regexp = "^[a-zA-ZčćžšđČĆŽŠĐ\\s]+$", message = "The name can only contain alphabetic characters.")
      String name,

      @NotBlank(message = "The giro account is a required field.")
      @Pattern(
            regexp = "^555-[0-9]{3}-[0-9]{8}-[0-9]{2}$",
            message = "Bank account number must be in the format 555-YYY-ZZZZZZZZ-WW"
      )
      @Size(min = 19, max = 19, message = "Bank account number must be exactly 19 characters long")
      String bankAccountNumber,

      @Pattern(
            regexp = "^(\\d{3}/\\d{3}-\\d{3})?$",
            message = "Fax number must be in the format XXX/YYY-ZZZ (e.g., 123/456-789)"
      )
      String fax
) {
}
public record BankUpdateRequest(
      @Length(max = 255, message = "The name cannot be longer than 255 characters.")
      @Pattern(regexp = "^[a-zA-ZčćžšđČĆŽŠĐ\\s]+$", message = "The name can only contain alphabetic characters.")
      String name,

      @Pattern(
            regexp = "^555-[0-9]{3}-[0-9]{8}-[0-9]{2}$",
            message = "Bank account number must be in the format 555-YYY-ZZZZZZZZ-WW"
      )
      @Size(min = 19, max = 19, message = "Bank account number must be exactly 19 characters long")
      String bankAccountNumber,

      @Pattern(
            regexp = "^(\\d{3}/\\d{3}-\\d{3})?$",
            message = "Fax number must be in the format XXX/YYY-ZZZ (e.g., 123/456-789)"
      )
      String fax
) {
}

I am repeating the bean validation for all fields here and there are also other places where I might have a name field, a bankAccountNumber etc. So I thought of using value objects instead, but by definition a value object cannot be null, which is in conflict with my requirements for the patch based update DTO, which does not require any particular values to be updated or to be present. I wanted to have something like this:

record BankCreateRequest(@NotNull Name name, @NotNull BankAccountNumber bankAccountNumber, Fax fax) {}

record BankUpdateRequest(Name name, BankAccountNumber bankAccountNumber, Fax fax) {}

And then have three dedicated records that check if those values are valid. Does this go against common best practices for value objects as they by definition cannot be null? Is there a better approach that is as simple?

I have the two following DTOs:

public record BankCreateRequest(
      @NotBlank(message = "The name is a required field.")
      @Length(max = 255, message = "The name cannot be longer than 255 characters.")
      @Pattern(regexp = "^[a-zA-ZčćžšđČĆŽŠĐ\\s]+$", message = "The name can only contain alphabetic characters.")
      String name,

      @NotBlank(message = "The giro account is a required field.")
      @Pattern(
            regexp = "^555-[0-9]{3}-[0-9]{8}-[0-9]{2}$",
            message = "Bank account number must be in the format 555-YYY-ZZZZZZZZ-WW"
      )
      @Size(min = 19, max = 19, message = "Bank account number must be exactly 19 characters long")
      String bankAccountNumber,

      @Pattern(
            regexp = "^(\\d{3}/\\d{3}-\\d{3})?$",
            message = "Fax number must be in the format XXX/YYY-ZZZ (e.g., 123/456-789)"
      )
      String fax
) {
}
public record BankUpdateRequest(
      @Length(max = 255, message = "The name cannot be longer than 255 characters.")
      @Pattern(regexp = "^[a-zA-ZčćžšđČĆŽŠĐ\\s]+$", message = "The name can only contain alphabetic characters.")
      String name,

      @Pattern(
            regexp = "^555-[0-9]{3}-[0-9]{8}-[0-9]{2}$",
            message = "Bank account number must be in the format 555-YYY-ZZZZZZZZ-WW"
      )
      @Size(min = 19, max = 19, message = "Bank account number must be exactly 19 characters long")
      String bankAccountNumber,

      @Pattern(
            regexp = "^(\\d{3}/\\d{3}-\\d{3})?$",
            message = "Fax number must be in the format XXX/YYY-ZZZ (e.g., 123/456-789)"
      )
      String fax
) {
}

I am repeating the bean validation for all fields here and there are also other places where I might have a name field, a bankAccountNumber etc. So I thought of using value objects instead, but by definition a value object cannot be null, which is in conflict with my requirements for the patch based update DTO, which does not require any particular values to be updated or to be present. I wanted to have something like this:

record BankCreateRequest(@NotNull Name name, @NotNull BankAccountNumber bankAccountNumber, Fax fax) {}

record BankUpdateRequest(Name name, BankAccountNumber bankAccountNumber, Fax fax) {}

And then have three dedicated records that check if those values are valid. Does this go against common best practices for value objects as they by definition cannot be null? Is there a better approach that is as simple?

Share Improve this question edited Mar 21 at 5:25 Basil Bourque 341k123 gold badges936 silver badges1.3k bronze badges asked Mar 20 at 15:38 aleksaleks 354 bronze badges 5
  • value object cannot be null, who is saying this? Probably you mean the components of a VO "should" not be null. Maybe. Depending on the invariants of the VO, a null component can be ok. Also, your Bank*Request.name can be null, if this makes sense in the app. – PeterMmm Commented Mar 20 at 18:50
  • Note to readers: Bean Validation is now known as Jakarta Validation. – Basil Bourque Commented Mar 21 at 5:24
  • Define what you mean by “value objects”. JEP 401? – Basil Bourque Commented Mar 21 at 5:28
  • “other places where I might have a name field, a bankAccountNumber, etc” — Perhaps those should each be a type, defined in a class, rather than be String? – Basil Bourque Commented Mar 21 at 5:33
  • @BasilBourque that was exactly my point. – aleks Commented Mar 25 at 9:15
Add a comment  | 

1 Answer 1

Reset to default 1

Using Value Objects (VOs) for validation is a great approach to encapsulate constraints and avoid repeated annotations. However, as you pointed out, a fundamental property of value objects is that they should not be null, which conflicts with the optional fields in your BankUpdateRequest.

Possible Approaches

Here are some ways to balance clean design with flexibility for updates:

1. Use Value Objects but Allow null in DTOs

  • Instead of making fields @NotNull in DTOs, allow null for update DTOs.

  • The validation logic moves to the VO constructors.

public record Name(String value) {     public Name {         if (value != null) {             if (value.isBlank()) throw new IllegalArgumentException("The name is a required field.");             if (value.length() > 255) throw new IllegalArgumentException("The name cannot be longer than 255 characters.");             if (!value.matches("^[a-zA-ZčćžšđČĆŽŠĐ\\s]+$"))                  throw new IllegalArgumentException("The name can only contain alphabetic characters.");         }     } } 

Then your DTOs become:

public record BankCreateRequest(@NotNull Name name, @NotNull BankAccountNumber bankAccountNumber, Fax fax) {}  public record BankUpdateRequest(Name name, BankAccountNumber bankAccountNumber, Fax fax) {} 
  • This way, BankCreateRequest enforces values.

  • BankUpdateRequest allows null, and validation happens only if values are present.

Pros:

  • Removes repetitive bean validation.

  • Encapsulates validation logic in VOs.

  • Works well with both create and update.

Cons:

  • You must manually handle null checks in constructors.

  • Validation happens at object creation rather than at request validation (which might be less intuitive for some teams).


2. Combine VOs with Jakarta Validation (formerly Bean Validation)

An alternative is keeping Spring’s validation for DTOs but still using Value Objects:

public record BankCreateRequest(     @NotNull Name name,     @NotNull BankAccountNumber bankAccountNumber,     Fax fax ) {}  public record BankUpdateRequest(     Name name,     BankAccountNumber bankAccountNumber,     Fax fax ) {} 

Then keep Hibernate Validator annotations inside the VOs:

@Value public class Name {     @NotBlank(message = "The name is a required field.")     @Size(max = 255, message = "The name cannot be longer than 255 characters.")     @Pattern(regexp = "^[a-zA-ZčćžšđČĆŽŠĐ\\s]+$", message = "The name can only contain alphabetic characters.")     String value; } 

Key Trick: Use @Valid in DTOs to trigger Spring validation:

public record BankCreateRequest(@Valid @NotNull Name name, @Valid @NotNull BankAccountNumber bankAccountNumber, @Valid Fax fax) {}  public record BankUpdateRequest(@Valid Name name, @Valid BankAccountNumber bankAccountNumber, @Valid Fax fax) {} 

Pros:

  • Fully integrates with Spring’s validation.

  • No need for manual exceptions in VOs.

  • Works well with null updates.

Cons:

  • Spring validation is still needed in VOs.

  • Slightly more boilerplate.


Which is Better?

  • Approach 1 (manual validation in VO constructor) is better if you want pure DDD-style Value Objects but requires handling null checks yourself.

  • Approach 2 (Spring Validation inside VOs) is simpler and integrates better with Spring Boot, so it’s often the more practical choice.

Recommendation: Use Approach 2 (@Valid + VOs). It’s simpler, aligns with Spring Boot, and still removes duplication.

发布评论

评论列表(0)

  1. 暂无评论