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 |1 Answer
Reset to default 1Using 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, allownull
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
allowsnull
, 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.
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, yourBank*Request.name
can be null, if this makes sense in the app. – PeterMmm Commented Mar 20 at 18:50name
field, abankAccountNumber, etc
” — Perhaps those should each be a type, defined in a class, rather than beString
? – Basil Bourque Commented Mar 21 at 5:33