Implementing Parameter Validation with Internationalization Support in Spring Boot

Parameter validation represents a fundamental aspect of building robust applications. Proper validation ensures that your code handles unexpected inputs gracefully, preventing runtime exceptions and maintaining system stability. When combined with internationalization capabilities, validation errors can communicate effectively with users across different locales, significantly improving the overall user experience.

This article explores various validation techniques available in Spring Boot applications, demonstrating how to integrate validation with internationalization to produce localized error messages. We'll cover built-in annotations, custom validation logic, exception handling strategies, and custom annotation creation through aspect-oriented programming.

Request Parameter Validation

The simplest form of parameter validation involves using Spring's @RequestParam annotation, which provides built-in support for marking parameters as required. The required attribute determines whether the parameter must be present in the request:

@GetMapping("/greeting")
public String getGreeting(@RequestParam(required = false, name = "name") String name) {
    if (StringUtils.isBlank(name)) {
        name = "World";
    }
    return String.format("Hello, %s!", name);
}

When the required attribute is set to false, the parameter becomes optional. If no value is provided, the method handles the null case gracefully by substituting a default value. This approach works well for scenarios where missing parameters don't require special error handling.

Using Built-in Validation Annotations

While @RequestParam verifies parameter presence, it doesn't validate the actual content. For more comprehensive checks, Spring Validation provides numerous annotations that can be applied directly to method parameters. The @NotEmpty annotation, for instance, ensures that string parameters contain actual content beyond whitespace:

@GetMapping("/greeting/required")
public String getRequiredGreeting(@RequestParam @NotEmpty String name) {
    return String.format("Hello, %s!", name);
}

Accessing this endpoint without the name parameter or with an empty value triggers a validation error. Spring's validation mechanism automatically detects the violation and prevents the method from executing.

Customizing Error Messages

Each validation annotation supports a message attribute that defines custom error text. This proves essential for providing user-friendly feedback:

@GetMapping("/greeting/validated")
public String getValidatedGreeting(@RequestParam @NotEmpty(message = "Name parameter cannot be empty") String name) {
    return String.format("Hello, %s!", name);
}

Spring Boot's validation module includes numerous built-in annotations such as @NotNull, @Size, @Min, @Max, @Email, and @Pattern. Each supports message customization and can be combined for complex validation scenarios.

Entity Class Validation

For complex request bodies, validating individual parameters becomes unwieldy. Instead, Spring supports validation through entity classes annotated with validation constraints. When combined with @Valid, these annotations trigger validation automatically before the method executes:

@PostMapping("/user/register")
public String registerUser(@RequestBody @Valid UserRegistrationRequest request) {
    return String.format("User registered: %s", request.getUsername());
}

public class UserRegistrationRequest {
    
    @NotBlank(message = "Username is required")
    @Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
    private String username;
    
    @NotBlank(message = "Email cannot be blank")
    @Email(message = "Invalid email format")
    private String email;
    
    // Getters and setters
    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;
    }
}

This approach centralizes validation logic within the entity class, promoting better code organization and reusability across multiple endpoints.

Custom Exception Handling

When built-in validation fails, Spring throws MethodArgumentNotValidException. However, many applications require custom exceptions to handle domain-specific error scenarios. Creating a dedicated exception class allows for precise error categorization:

public class ValidationException extends RuntimeException {
    
    private final String errorCode;
    
    public ValidationException(String errorCode) {
        super(errorCode);
        this.errorCode = errorCode;
    }
    
    public String getErrorCode() {
        return errorCode;
    }
}

Controllers can now throw custom exceptions that represent specific business rule violations, which centralized handlers can then process appropriately.

Global Exception Handler

A global exception handler ensures consistent error responses across your application. The @RestControllerAdvice annotation enables a centralized component that intercepts exceptions from all controllers:

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    private final MessageSource messageSource;
    
    public GlobalExceptionHandler(MessageSource messageSource) {
        this.messageSource = messageSource;
    }
    
    @ExceptionHandler(ValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiErrorResponse handleValidationException(ValidationException ex, Locale locale) {
        String localizedMessage = messageSource.getMessage(ex.getErrorCode(), null, locale);
        return new ApiErrorResponse(400, localizedMessage);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiErrorResponse handleArgumentException(MethodArgumentNotValidException ex, Locale locale) {
        StringBuilder errorMessage = new StringBuilder();
        ex.getBindingResult().getFieldErrors().forEach(error -> 
            errorMessage.append(messageSource.getMessage(error.getDefaultMessage(), null, locale))
        );
        return new ApiErrorResponse(400, errorMessage.toString());
    }
}

public class ApiErrorResponse {
    private final int status;
    private final String message;
    
    public ApiErrorResponse(int status, String message) {
        this.status = status;
        this.message = message;
    }
    
    public int getStatus() {
        return status;
    }
    
    public String getMessage() {
        return message;
    }
}

Internationalization Configuration

To support multiple languages, configure Spring's message source with appropriate message bundles. Messages are stored in property files named according to locale conventions:

server:
  port: 8080
  servlet:
    context-path: /api
    
spring:
  messages:
    basename: i18n/messages
    encoding: UTF-8
    fallback-to-system-locale: false
  mvc:
    locale: en_US
    locale-resolver: accept-header

The basename property specifies the path and prefix for message files. For example, messages_en_US.properties contains English messages, while messages_zh_CN.properties contains Chinese translations. When a request arrives, Spring automatically selects the appropriate locale based on the Accept-Language header.

Create message bundle files with key-value pairs representing error messages:

# messages_en_US.properties
username.required=Username is required
email.invalid=Please provide a valid email address
field.required=The {0} field cannot be empty

# messages_zh_CN.properties
username.required=用户名不能为空
email.invalid=请输入有效的电子邮箱地址
field.required={0}字段不能为空

The placeholder syntax {0} allows dynamic parameter insertion, enabling field-specific error messages that adapt to the validated property name.

Custom Validation Annotations

When built-in annotations don't cover specific validation requirements, custom annotations provide flexibility. Consider a scenario where you need consistent required-field validation across multiple entity classes with automatic field name inclusion in error messages:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = RequiredFieldValidator.class)
public @interface RequiredField {
    String message() default "Field is required";
    
    Class<?>[] groups() default {};
    
    Class<? extends Payload>[] payload() default {};
}

The validator class implements the actual validation logic:

public class RequiredFieldValidator implements ConstraintValidator<RequiredField, Object> {
    
    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        return value != null && !(value instanceof String && ((String) value).trim().isEmpty());
    }
}

However, for more complex scenarios requiring dynamic message resolution with field names, an aspect-based approach offers greater flexibility:

@Aspect
@Component
public class RequiredFieldAspect {
    
    private final MessageSource messageSource;
    
    public RequiredFieldAspect(MessageSource messageSource) {
        this.messageSource = messageSource;
    }
    
    @Around("execution(* com.example..controller.*.*(..)) && args(.., request)")
    public Object validateRequiredFields(ProceedingJoinPoint joinPoint, Object request) throws Throwable {
        Field[] fields = request.getClass().getDeclaredFields();
        
        for (Field field : fields) {
            if (field.isAnnotationPresent(NotBlank.class)) {
                field.setAccessible(true);
                Object fieldValue = field.get(request);
                
                if (isEmpty(fieldValue)) {
                    String fieldName = field.getName();
                    String errorKey = "field.required";
                    Locale currentLocale = LocaleContextHolder.getLocale();
                    String errorMessage = messageSource.getMessage(errorKey, new Object[]{fieldName}, currentLocale);
                    
                    throw new ValidationException("field.required");
                }
            }
        }
        
        return joinPoint.proceed();
    }
    
    private boolean isEmpty(Object value) {
        if (value == null) {
            return true;
        }
        if (value instanceof String) {
            return ((String) value).trim().isEmpty();
        }
        return false;
    }
}

This aspect intercepts controller method calls, inspects the request object for annotated fields, and performs validation before the actual method executes. When validation fails, it retrieves the appropriate localized message and throws a custom exception.

Combining Techniques

The most robust applications combine multiple validation strategies. Use built-in annotations for common validation scenarios, entity-level validation for complex objects, custom exceptions for business rule violations, and aspects for cross-cutting validation concerns. This layered approach provides comprehensive validation coverage while maintaining clean, maintainable code.

Consider a registration endpoint that employs multiple validation techniques:

@PostMapping("/register")
public ResponseEntity<ApiErrorResponse> registerUser(
        @RequestBody @Valid UserRegistrationRequest request,
        Locale locale) {
    
    if (!passwordMeetsComplexity(request.getPassword())) {
        throw new ValidationException("password.complexity");
    }
    
    userService.createUser(request);
    return ResponseEntity.ok(new ApiErrorResponse(200, "Registration successful"));
}

The @Valid annotation triggers entity-level validation, while the aspect handles additional field-level checks. Custom exceptions capture business rule violations, and the global handler formats all errors consistently with appropriate localization.

Tags: spring-boot Validation Internationalization jakarta-validation aspect-oriented-programming

Posted on Thu, 11 Jun 2026 16:14:11 +0000 by Niccaman