package com.example.auth.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
public class SignupRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 30, message = "Username must be between 3 and 30 characters")
private String username;
@NotBlank(message = "Email is required")
@Email(message = "Email must be a valid address")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
@Pattern(regexp = ".*\\d.*", message = "Password must contain at least one digit")
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
package com.example.auth.web;
import com.example.auth.dto.SignupRequest;
import com.example.auth.service.UserService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserService userService;
public AuthController(UserService userService) {
this.userService = userService;
}
@PostMapping("/signup")
public ResponseEntity<Long> register(@Valid @RequestBody SignupRequest request) {
Long userId = userService.createAccount(
request.getUsername(),
request.getEmail(),
request.getPassword());
return ResponseEntity.status(HttpStatus.CREATED).body(userId);
}
}
package com.example.auth.web;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.Instant;
import java.util.Map;
import java.util.stream.Collectors;
@RestControllerAdvice
public class ValidationExceptionHandler {
public record ApiError(String message, Instant timestamp, Map<String, String> fieldErrors) {
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> fieldErrors = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
error -> error.getDefaultMessage() == null ? "Invalid value" : error.getDefaultMessage(),
(first, second) -> first));
ApiError body = new ApiError("Validation failed", Instant.now(), fieldErrors);
return ResponseEntity.badRequest().body(body);
}
}
This snippet shows the standard way a Spring Boot service validates an incoming signup request declaratively and turns any constraint violations into a clean, structured JSON error payload. The core idea is to keep validation rules on the data itself rather than scattering if checks through controller code, so the request object becomes the single source of truth for what a valid signup looks like.
In SignupRequest, the DTO carries Jakarta Bean Validation annotations — @NotBlank, @Email, @Size, and @Pattern — each with a human-readable message. These are metadata, not executed logic; Hibernate Validator (the reference implementation bundled with spring-boot-starter-validation) reads them at runtime. A subtle but important detail is @Size(min = 8) combined with @NotBlank on the password: @NotBlank guards the empty case while @Size enforces length, because a single annotation cannot express both concerns cleanly.
In AuthController, the @Valid annotation on the @RequestBody parameter is what actually triggers validation. When the bound object fails, Spring does not proceed to the method body — it throws a MethodArgumentNotValidException before register runs. That is why the controller itself contains no validation code at all; the happy path assumes the data is already trustworthy, which keeps business logic readable.
The interesting work happens in ValidationExceptionHandler, a @RestControllerAdvice that centralizes error formatting for every controller in the application. Its @ExceptionHandler for MethodArgumentNotValidException walks getBindingResult().getFieldErrors() and collapses them into a field -> message map. Using a merge function in Collectors.toMap avoids an IllegalStateException when one field has multiple failing constraints, keeping only the first message per field. The response is wrapped in an ApiError record and returned with 400 Bad Request.
This pattern scales well: adding a new rule means adding one annotation, and the error shape stays consistent across endpoints. The main trade-offs are that annotation order does not guarantee message order, cross-field rules (like password confirmation) need a custom class-level constraint instead, and messages are best externalized to a messages.properties file for internationalization. It is the go-to approach whenever request payloads have well-defined, per-field rules.
Related snips
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
class CreateDeadJobs < ActiveRecord::Migration[7.1]
def change
create_table :dead_jobs do |t|
t.string :jid, null: false
t.string :queue, null: false
t.string :klass, null: false
Background Job Dead Letter Queue (DLQ) Table
Share this code
Here's the card — post it anywhere.