Docs LogoDocs
Spring Boot NotesIntermediate

DTOs & Request Validation

Using DTOs instead of exposing entities, and Bean Validation annotations.

DTOs & Request Validation

Why DTOs (Data Transfer Objects)?

Never expose JPA entities directly in your API:

  • Entities carry persistence concerns (lazy-loaded relations → serialization bugs or LazyInitializationException).
  • Couples your API contract to your database schema — a column rename breaks the API.
  • Can leak internal/sensitive fields (password hashes, internal flags).

Use separate Request and Response DTOs (records are a great fit):

public record TransactionRequest(
        @NotBlank String description,
        @NotNull @Positive BigDecimal amount,
        @NotNull LocalDate date,
        @NotBlank String category
) {}

public record TransactionResponse(
        Long id,
        String description,
        BigDecimal amount,
        LocalDate date,
        String category,
        Instant createdAt
) {}

Map between entity ↔ DTO manually, with MapStruct, or with a small mapper class/method — avoid putting mapping logic in the controller.

Bean Validation Annotations

AnnotationChecks
@NotNullValue is not null
@NotBlankString is not null and not just whitespace
@NotEmptyCollection/String is not null and has ≥1 element
@Size(min, max)Length/size bounds
@Min / @MaxNumeric bounds
@Positive / @PositiveOrZeroNumeric sign checks
@EmailValid email format
@Pattern(regexp = ...)Regex match
@Past / @FutureDate constraints

Triggering Validation

@PostMapping
public TransactionResponse create(@Valid @RequestBody TransactionRequest req) {
    ...
}

@Valid triggers validation; a failure throws MethodArgumentNotValidException, which you handle globally (see the Exception Handling topic) to return a clean 400 with field errors.

Custom Validators

For rules that don't fit a built-in annotation:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ValidCategoryValidator.class)
public @interface ValidCategory {
    String message() default "Invalid category";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class ValidCategoryValidator implements ConstraintValidator<ValidCategory, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext ctx) {
        return CategoryRegistry.isKnown(value);
    }
}

Validation Groups (advanced but useful)

Use different validation rules for create vs update (e.g. id required on update, forbidden on create) via @Validated(OnCreate.class) and marker interfaces — avoids duplicating near-identical DTOs.

Last updated on July 15, 2026

On this page