Spring Boot Input Validation: A Complete Guide

Spring Boot Input Validation: A Complete Guide

Input validation is a critical step in building secure and reliable Spring Boot applications. It ensures incoming data is accurate, meets defined rules, and prevents potential vulnerabilities like injection attacks or invalid formats. Spring Boot uses the Jakarta Bean Validation API (formerly JSR 303/380) with Hibernate Validator as the default implementation, allowing you to validate data across controllers, services, and persistence layers.

Here’s a quick overview of what you’ll learn:

  • Core Validation Annotations: Use @NotNull, @Email, @Size, and others to enforce constraints on fields.
  • Cascading Validation: Leverage @Valid for nested objects and collections.
  • Custom Validators: Create tailored annotations for business-specific rules.
  • Validation Groups: Apply different rules for create and update operations.
  • Error Handling: Centralise exception handling with @ControllerAdvice and customise error messages for clarity.
  • Setup Notes: Since Spring Boot 2.3, you must explicitly add the spring-boot-starter-validation dependency.

Bean Validation Annotations in Spring Boot

spring boot homepage

Spring Boot leverages the Jakarta Bean Validation API (formerly JSR 303/380) to enforce constraints on model fields. These annotations act like rules that the validator – usually Hibernate Validator – checks at runtime. As Tom Hombergs, a software engineer, explains:

“Bean Validation is the de-facto standard for implementing validation logic in the Java ecosystem”.

This framework comes with a wide range of annotations, handling everything from simple null checks to more intricate pattern matching. Knowing which annotation to use and when can make your validation logic both accurate and easier to maintain. Below, we’ll explore how field-level annotations help define these constraints.

Field-Level Annotations

Field-level annotations are applied directly to class properties, specifying the conditions that must be satisfied before data is processed. Here’s a breakdown of some commonly used ones:

  • String validation: Different annotations cater to varying requirements.
    • @NotNull ensures the field is not null but allows empty strings or whitespace.
    • @NotEmpty requires at least one character, though it doesn’t rule out whitespace-only values.
    • @NotBlank is stricter, rejecting null, empty, and whitespace-only strings. It’s the go-to choice for mandatory text fields.
  • Numeric constraints:
    • @Min and @Max set inclusive boundaries, such as @Min(18) for age restrictions.
    • @Positive and @Negative enforce values strictly greater or less than zero.
    • @Size checks the length or size of strings, collections, maps, or arrays, ensuring they fall within a defined range.
  • Format validation:
    • @Email validates standard email addresses.
    • @Pattern allows custom regular expressions, making it ideal for specific formats like postcodes or phone numbers.
    • Temporal annotations like @Past and @Future ensure dates align with expected timeframes.

Comparison of String Validation Annotations

AnnotationAllows Null?Allows Empty?Allows Whitespace?
@NotNullNoYesYes
@NotEmptyNoNoYes
@NotBlankNoNoNo

To make error messages more user-friendly, you can customise them using the message attribute. For instance, @NotNull(message = "Name is mandatory") replaces the default system message with something clearer.

Using @Valid for Object Validation

In addition to field-level checks, Spring Boot supports cascading validation with @Valid. This annotation validates method parameters and enables nested object validation. When placed on a controller’s @RequestBody parameter, Spring Boot automatically validates the object before executing the method. If validation fails, it throws a MethodArgumentNotValidException.

The real power of @Valid shines when dealing with nested objects. For example, if a User object contains an Address object, you can annotate the Address field with @Valid to ensure that constraints like @NotBlank or @Size within the Address class are also validated.

You can even validate elements within collections. For example, List<@NotBlank String> tags ensures that each string in the list meets the @NotBlank criteria. This approach keeps validation centralised and neatly integrated into your domain models.

Setting Up Input Validation in Spring Boot

Getting input validation working in Spring Boot is pretty simple. But here’s an important note: since Spring Boot 2.3, validation isn’t included automatically anymore. Earlier versions bundled it with spring-boot-starter-web, but from 2.3 onwards, you’ll need to add the validation starter manually.

The key dependency you’ll need is spring-boot-starter-validation. This brings in everything required, including the Jakarta Validation API and Hibernate Validator, which is the default implementation used by Spring Boot. If you’re using Maven, you can add it to your pom.xml like this:

<dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-validation</artifactId> </dependency> 

For those starting a project with Spring Initializr, just search for “Validation” and tick the box – it’ll handle the dependency for you.

Once the dependency is added, Spring Boot takes care of the setup. It automatically configures a LocalValidatorFactoryBean as the global validator. This bean integrates Hibernate Validator with Spring’s validation framework, so you don’t need to write any additional configuration or XML files. Plus, it links your app’s MessageSource to the validator, which means you can define custom error messages in a messages.properties file, and they’ll be resolved automatically.

To validate input in controllers, use the @Valid annotation on method parameters. For service layer validation, annotate your class with @Validated to enable method validation through AOP proxies.

With this setup, you’re ready to use validation across your application layers.

Applying Validation Across Application Layers

Validation is a crucial step wherever data enters your application. With Spring Boot, you can implement it at multiple points – whether it’s a REST endpoint, a service method, or even before data reaches the database.

Controller-Level Validation

Controllers are your first line of defence for validating incoming data. By annotating JSON @RequestBody parameters with @Valid, you can trigger validation of DTO fields before the controller method executes. If something doesn’t meet the requirements, Spring will throw a MethodArgumentNotValidException.

For path variables and query parameters, you can annotate the controller class with @Validated and then apply constraint annotations like @Min or @Size directly on the method parameters. If these validations fail, Spring responds with a ConstraintViolationException, which results in an HTTP 500 status unless you handle it explicitly.

Here’s a quick summary of how exceptions are tied to validation:

Validation TargetRequired Annotation(s)Exception Thrown
Request Body@Valid or @Validated on the parameterMethodArgumentNotValidException
Path Variable@Validated on the class + parameter constraintConstraintViolationException
Request Parameter@Validated on the class + parameter constraintConstraintViolationException

If your DTO includes nested objects or collections, you can cascade validation by annotating those fields with @Valid.

While controllers handle external input, validation shouldn’t stop there. The service layer plays a key role in maintaining data consistency within your application.

Service-Level Validation

Controllers might catch issues at the entry point, but the service layer acts as a second checkpoint. Data doesn’t only come from REST endpoints – it could originate from scheduled tasks, message queues, or other internal components. That’s why validation at this level is equally important.

To enable method-level validation in your service layer, annotate the @Service class with @Validated. Then, use standard JSR-303 annotations like @NotNull, @Min, and @Size on the method arguments. Spring uses AOP proxies to validate these arguments before the method executes. If any constraint is violated, a ConstraintViolationException is thrown.

For more granular control, you can inject a jakarta.validation.Validator into your service. This allows you to validate objects at any point in your business logic. For example, after modifying an object but before saving it to the database, you can call validator.validate(myObject) and review the resulting set of constraint violations to ensure everything checks out.

Building Custom Validation Annotations

Custom validation annotations are a powerful way to enforce rules specific to your application’s needs. While standard annotations like @NotNull and @Email handle common cases, they often fall short when it comes to business-specific requirements. With custom annotations, you can declare these rules cleanly, avoiding repetitive imperative checks.

Creating a custom validator involves two components: an annotation interface and a logic class. The annotation is defined using @interface and must include three key attributes: message() for the default error message, groups() for validation sequencing, and payload() for attaching custom metadata. To link it with its validation logic, use @Constraint(validatedBy = YourValidatorClass.class). The logic class itself implements ConstraintValidator<AnnotationType, FieldType> and overrides the isValid() method, which returns a boolean indicating whether the data is valid.

Since Spring manages validators as beans, you can inject services or repositories directly into them. This makes it easy to perform database checks, such as verifying if a username is unique. Below, we’ll explore how to create both field-level and class-level custom validators that work seamlessly in a Spring Boot environment.

Field-Level Custom Validators

Field-level validators are designed to validate individual properties. They use @Target(ElementType.FIELD) in their annotation definitions, making them perfect for enforcing rules on specific fields. For instance, if VIP customer IDs in your application must start with a particular prefix, you can create a custom annotation to check for that pattern. The isValid() method in this case only evaluates the field value.

One important practice is to treat null as valid in your isValid() method. This allows other annotations, like @NotNull, to handle null checks. As Alexander Obregon explains:

“Notice that we treat null as valid. That’s a common pattern, because constraints like @NotNull are meant to handle null values. You don’t want your validator to block null unless that’s part of its purpose”.

Another crucial consideration is thread safety. Spring treats validator classes as singletons, meaning a single instance is reused across requests. Aquib Javed highlights the risks of this:

“Spring treats Validator classes as Singletons. The container creates one instance and reuses it for every request. The Risk: Storing request-specific data in instance variables causes race conditions”.

To avoid such issues, ensure your validators are stateless by using only local variables in the isValid() method.

Class-Level Custom Validators

Class-level validators are used when validation depends on the relationship between multiple fields. For example, if you need to ensure a password matches its confirmation field, a class-level validator is the way to go. These validators use @Target(ElementType.TYPE) and have access to the entire object, making it easier to apply cross-field rules.

To access field values, you can use getters or BeanWrapperImpl on the object passed to isValid(). This approach allows you to compare fields and implement complex business rules in one central place. Saurav Sanyal offers this advice:

“Validators should focus only on validation and avoid modifying the state or triggering external calls unnecessarily”.

Keep your logic focused on validation to avoid unintended side effects. The table below summarises the differences between field-level and class-level validators:

FeatureField-Level ValidatorClass-Level Validator
TargetElementType.FIELDElementType.TYPE
ScopeValidates a single fieldValidates multiple fields
AccessReceives the field valueReceives the entire object
Common Use CaseString patterns, domain-specific IDsPassword confirmation, cross-field rules

Handling Validation Errors

spring boot validation

Spring Boot Validation Exceptions and Handling Guide

When validation fails in Spring Boot, specific exceptions are thrown, and it’s essential to handle them consistently across your application. The two most common exceptions are MethodArgumentNotValidException, which occurs when validating objects (like those annotated with @RequestBody), and HandlerMethodValidationException, which is triggered when constraints are applied directly to method parameters. The type of exception depends on the structure of your controller method.

A clean and efficient way to manage these errors is by using @ControllerAdvice along with @ExceptionHandler methods. This approach centralises error handling, removing the need for repetitive try-catch blocks in each controller method.

Spring also provides MessageSource to resolve validation messages through messages.properties. This feature supports internationalisation and lets you customise error codes flexibly. For example, a single validation error can generate multiple codes, such as Size.person.name, Size.name, and Size. This allows you to decide whether your error messages should be specific or more general. Let’s dive deeper into handling one of the most common exceptions – MethodArgumentNotValidException.

Handling MethodArgumentNotValidException

To handle MethodArgumentNotValidException, create a @ControllerAdvice class and use @ExceptionHandler. You can extract field errors using ex.getBindingResult().getFieldErrors() and map them into a custom error response object for better clarity. Each FieldError includes the field name and its corresponding error message, which can be structured into a custom response format – such as a ValidationErrorResponse. This response could include a list of violations, each detailing the field name and error message, making it easier for front-end developers to display errors next to the relevant form fields.

For added convenience, you can extend ResponseEntityExceptionHandler, which provides a solid foundation for handling standard Spring MVC exceptions, including validation errors.

Customising Error Messages and Responses

Customising error messages can be done at several levels. The simplest way is by using the message attribute in validation annotations (e.g., @NotBlank(message = "Name is mandatory")). For more advanced customisation, you can externalise messages to messages.properties files. Spring’s MessageSource will resolve these messages based on the client’s locale.

For example, in your messages.properties file, you could add an entry like this:

Size.person.name=Please, provide a {0} that is between {2} and {1} characters long 

This allows you to create dynamic and user-friendly error messages. As the Spring Boot documentation explains:

“The application’s MessageSource is used when resolving {parameters} in constraint messages. This allows you to use your application’s messages.properties files for Bean Validation messages”.

When designing your global exception handler, make sure it handles both MethodArgumentNotValidException and ConstraintViolationException. The latter is often thrown for path variables and request parameters. By addressing both, you can ensure consistent and unified error responses across all input types.

Using Validation Groups for Different Scenarios

Validation groups let you apply different validation rules to the same object, depending on the situation. Take an id field, for instance: it might be allowed to remain null when creating a new record (since the database generates it), but it must be non-null when updating an existing one. This is where validation groups prove useful.

Part of the Bean Validation API (JSR 380), validation groups let you organise constraints into separate categories. This means you can use a single object for various operations, applying specific validation rules based on the context. By default, standard validation annotations belong to the jakarta.validation.groups.Default group unless assigned otherwise.

To enable group-specific validation, you’ll need to use Spring’s @Validated annotation instead of the more common @Valid. The reason? @Valid doesn’t support group-specific rules. With @Validated, you can specify which group of rules to apply for a given controller method. Let’s explore how to set this up effectively.

Defining and Using Validation Groups

Start by defining marker interfaces to represent different validation scenarios. These interfaces act as labels and don’t require any methods. For example, you could create OnCreate and OnUpdate interfaces to differentiate between creating and updating operations.

Once you’ve set up these marker interfaces, you can assign them to specific constraints using the groups attribute in your validation annotations. For instance, you might use @NotNull(groups = OnUpdate.class) to ensure a field is non-null during an update. If you need to validate both group-specific constraints and default constraints together, include the Default group in your @Validated annotation, like so: @Validated({OnUpdate.class, Default.class}).

Validation Groups for CRUD Operations

A common use of validation groups is distinguishing between create and update operations. This method complements other validation layers in your application, such as those at the controller or service level, by tailoring rules to the operation context. For example, you could annotate the id field of a Person DTO with @Null(groups = OnCreate.class) for creation and @NotNull(groups = OnUpdate.class) for updates.

In your controller, you can then use @Validated(OnCreate.class) for the create endpoint and @Validated(OnUpdate.class) for the update endpoint. This keeps your validation logic streamlined and avoids duplicating DTOs. If you need to enforce a specific order of validation – for instance, running basic checks before more complex domain-specific ones – you can use @GroupSequence to define the sequence in which validations should occur.

Advanced Validation Techniques

Expanding on standard and group validations, these techniques take input validation to the next level, offering more flexibility and precision.

Customising the Validator

To globally customise the default Validator instance, implement the WebMvcConfigurer interface and override the getValidator() method. This allows you to replace the default validator with a custom instance, such as an OptionalValidatorFactoryBean, which will be applied across all controllers using @Valid and @Validated. If you need to tweak the ValidatorFactory setup, define a ValidationConfigurationCustomizer bean. When multiple customisers are present, the @Order annotation helps control the sequence in which they are applied.

For more targeted customisation, you can use the @InitBinder annotation within a specific controller. This lets you add local Validator implementations to a WebDataBinder via binder.addValidators(), ensuring that the adjustments stay confined to that controller. If your custom LocalValidatorFactoryBean is shared with other services, define it as a bean and mark it with @Primary to avoid conflicts with the default validator configured by Spring MVC. Additionally, Spring’s configuration supports dependency injection for custom validators, making it easier to integrate them into your application.

With these customisation options, you can fine-tune validation behaviour and even adapt error messages to suit different user groups.

Internationalising Validation Messages

To support multiple languages in validation messages, create resource bundle files named using the basename_locale.properties convention. For example, messages.properties serves as the default fallback, while messages_fr.properties contains French translations. Be sure to encode these files in UTF-8 for compatibility.

Set up a ReloadableResourceBundleMessageSource bean to resolve message keys, then register it with a LocalValidatorFactoryBean using the setValidationMessageSource() method. This setup ensures that validation messages remain user-friendly and context-aware. In validation annotations, reference the resource bundle keys using curly braces, such as @NotBlank(message = "{user.name.required}").

Spring Boot’s LocaleResolver automatically detects the client’s language preference via the Accept-Language HTTP header and selects the appropriate locale. You can even use placeholders like {min} or {max} in your messages, which are dynamically replaced with the actual constraint values during runtime. This approach not only enhances user experience but also ensures clarity across diverse audiences.

Conclusion

Input validation is a cornerstone of securing Spring Boot applications and ensuring data integrity. As Lucien Chemaly from Snyk points out:

“Validation plays a key role in preventing injection attacks and helps prevent security vulnerabilities such as stored cross-site scripting (XSS)”.

By using the techniques outlined in this guide, you’re not just safeguarding your application from harmful inputs but also ensuring your data remains accurate and reliable.

The Jakarta Bean Validation API, alongside Hibernate Validator as its reference implementation, offers a consistent, annotation-based approach that integrates smoothly across all layers of your application. With tools like field-level constraints, custom validators, and validation groups, you’re equipped to meet a variety of business needs effectively.

One key principle to keep in mind is to validate at the edge – in your controllers – before any data reaches your business logic. As Tom Hombergs from Reflectoring.io explains:

“We usually don’t want to do validation as late as in the persistence layer because it means that the business code above has worked with potentially invalid objects which may lead to unforeseen errors”.

Additionally, centralised exception handling using @ControllerAdvice ensures your API returns clear, consistent error messages, making it easier for developers to interact with your endpoints.

To summarise the key practices: use standard annotations for straightforward scenarios, create custom validators for more complex rules, employ validation groups for different contexts, and ensure error messages are internationalised to cater to a broad user base.

By consistently applying these strategies, you’re not just securing your application; you’re building trust with your users. Input validation isn’t merely about keeping errors at bay – it’s about upholding the integrity of your system from end to end.

At Tucanoo Solutions Ltd, we prioritise these validation practices in every Spring Boot application we develop, ensuring they are secure, resilient, and capable of meeting the demands of today’s digital landscape.

FAQs

How can I implement input validation in a Spring Boot project?

To set up input validation in a Spring Boot project, start by including a JSR-303 implementation, such as Hibernate Validator, in your project dependencies. Spring Boot automatically activates Bean Validation support when it detects this dependency.

You can then add validation annotations like @NotNull, @Size, or @Min to your data models or method parameters. These annotations define the rules for validating inputs. If you need to validate method parameters, annotate your class with @Validated and apply the constraints within your service or controller methods.

Spring Boot takes care of validating inputs against the specified rules. If any validation errors occur, they will be handled automatically, ensuring your application processes inputs securely and reliably.

What is the difference between field-level and class-level custom validators in Spring Boot?

Field-level custom validators focus on validating individual fields within a class. For example, they can ensure a string isn’t empty or a number falls within a specific range. These validators are applied directly to the fields themselves using annotations, making them ideal for straightforward, field-specific checks.

In contrast, class-level custom validators assess the entire object. They’re particularly useful when validation involves the relationship between multiple fields. For instance, you might need to confirm that one date occurs before another or that two fields maintain consistency. This type of validation handles more complex logic that goes beyond checking individual fields.

Both approaches play a key role in creating dependable Spring Boot applications. They help maintain data accuracy and ensure compliance with business rules.

How can I ensure consistent handling of validation errors in my Spring Boot application?

To manage validation errors effectively in a Spring Boot application, you can implement a global exception handler using the @ControllerAdvice annotation. This approach lets you catch exceptions like MethodArgumentNotValidException or ConstraintViolationException and return consistent, standardised error responses.

Centralising error handling not only helps in delivering clear and uniform feedback to users but also simplifies your codebase, making it easier to maintain and manage across the application.

 

David Brown founded Tucanoo Solutions in 2011 and has written software in a professional capacity for over 30 years, back to the Java/J2EE days of Sun Microsystems. He builds cloud and web applications for clients in legal tech, government and beyond, including several years consulting to the United Nations on customs and transport systems. He writes these tutorials himself, with the full code on GitHub.

Leave a Reply

Your email address will not be published. Required fields are marked *