3.16 Bean Validation

Since Micronaut 1.2, Micronaut has built-in support for validating beans annotated with javax.validation annotations. At a minimum include the micronaut-validation module as a compile dependency:

  1. implementation("io.micronaut:micronaut-validation")
  1. <dependency>
  2. <groupId>io.micronaut</groupId>
  3. <artifactId>micronaut-validation</artifactId>
  4. </dependency>

Note that Micronaut’s implementation is not currently fully compliant with the Bean Validator specification as the specification heavily relies on reflection-based APIs.

The following features are unsupported at this time:

  • Annotations on generic argument types, since only the Java language supports this feature.

  • Any interaction with the constraint metadata API, since Micronaut uses compile-time generated metadata.

  • XML-based configuration

  • Instead of using javax.validation.ConstraintValidator, use ConstraintValidator (io.micronaut.validation.validator.constraints.ConstraintValidator) to define custom constraints, which supports validating annotations at compile time.

Micronaut’s implementation includes the following benefits:

  • Reflection and Runtime Proxy free validation, resulting in reduced memory consumption

  • Smaller JAR size since Hibernate Validator adds another 1.4MB

  • Faster startup since Hibernate Validator adds 200ms+ startup overhead

  • Configurability via Annotation Metadata

  • Support for Reactive Bean Validation

  • Support for validating the source AST at compile time

  • Automatic compatibility with GraalVM native without additional configuration

If you require full Bean Validator 2.0 compliance, add the micronaut-hibernate-validator module to your build, which replaces Micronaut’s implementation.

  1. implementation("io.micronaut.beanvalidation:micronaut-hibernate-validator")
  1. <dependency>
  2. <groupId>io.micronaut.beanvalidation</groupId>
  3. <artifactId>micronaut-hibernate-validator</artifactId>
  4. </dependency>

Validating Bean Methods

You can validate methods of any class declared as a Micronaut bean by applying javax.validation annotations to arguments:

  1. import jakarta.inject.Singleton;
  2. import javax.validation.constraints.NotBlank;
  3. @Singleton
  4. public class PersonService {
  5. public void sayHello(@NotBlank String name) {
  6. System.out.println("Hello " + name);
  7. }
  8. }
  1. import jakarta.inject.Singleton
  2. import javax.validation.constraints.NotBlank
  3. @Singleton
  4. class PersonService {
  5. void sayHello(@NotBlank String name) {
  6. println "Hello $name"
  7. }
  8. }
  1. import jakarta.inject.Singleton
  2. import javax.validation.constraints.NotBlank
  3. @Singleton
  4. open class PersonService {
  5. open fun sayHello(@NotBlank name: String) {
  6. println("Hello $name")
  7. }
  8. }

The above example declares that the @NotBlank annotation will be validated when invoking the sayHello method.

If you use Kotlin, the class and method must be declared open so Micronaut can create a compile-time subclass. Alternatively you can annotate the class with @Validated and configure the Kotlin all-open plugin to open classes annotated with this type. See the Compiler plugins section.

A javax.validation.ConstraintViolationException is thrown if a validation error occurs. For example:

  1. import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
  2. import org.junit.jupiter.api.Test;
  3. import jakarta.inject.Inject;
  4. import javax.validation.ConstraintViolationException;
  5. import static org.junit.jupiter.api.Assertions.assertEquals;
  6. import static org.junit.jupiter.api.Assertions.assertThrows;
  7. @MicronautTest
  8. class PersonServiceSpec {
  9. @Inject PersonService personService;
  10. @Test
  11. void testThatNameIsValidated() {
  12. final ConstraintViolationException exception =
  13. assertThrows(ConstraintViolationException.class, () ->
  14. personService.sayHello("") (1)
  15. );
  16. assertEquals("sayHello.name: must not be blank", exception.getMessage()); (2)
  17. }
  18. }
  1. import io.micronaut.test.extensions.spock.annotation.MicronautTest
  2. import spock.lang.Specification
  3. import jakarta.inject.Inject
  4. import javax.validation.ConstraintViolationException
  5. @MicronautTest
  6. class PersonServiceSpec extends Specification {
  7. @Inject PersonService personService
  8. void "test person name is validated"() {
  9. when:"The sayHello method is called with a blank string"
  10. personService.sayHello("") (1)
  11. then:"A validation error occurs"
  12. def e = thrown(ConstraintViolationException)
  13. e.message == "sayHello.name: must not be blank" // (2)
  14. }
  15. }
  1. import io.micronaut.test.extensions.junit5.annotation.MicronautTest
  2. import org.junit.jupiter.api.Assertions.assertEquals
  3. import org.junit.jupiter.api.Assertions.assertThrows
  4. import org.junit.jupiter.api.Test
  5. import jakarta.inject.Inject
  6. import javax.validation.ConstraintViolationException
  7. @MicronautTest
  8. class PersonServiceSpec {
  9. @Inject
  10. lateinit var personService: PersonService
  11. @Test
  12. fun testThatNameIsValidated() {
  13. val exception = assertThrows(ConstraintViolationException::class.java) {
  14. personService.sayHello("") (1)
  15. }
  16. assertEquals("sayHello.name: must not be blank", exception.message) (2)
  17. }
  18. }
1The method is called with a blank string
2An exception occurs

Validating Data Classes

To validate data classes, e.g. POJOs (typically used in JSON interchange), the class must be annotated with @Introspected (see the previous section on Bean Introspection) or, if the class is external, be imported by the @Introspected annotation.

  1. import io.micronaut.core.annotation.Introspected;
  2. import javax.validation.constraints.Min;
  3. import javax.validation.constraints.NotBlank;
  4. @Introspected
  5. public class Person {
  6. private String name;
  7. @Min(18)
  8. private int age;
  9. @NotBlank
  10. public String getName() {
  11. return name;
  12. }
  13. public int getAge() {
  14. return age;
  15. }
  16. public void setName(String name) {
  17. this.name = name;
  18. }
  19. public void setAge(int age) {
  20. this.age = age;
  21. }
  22. }
  1. import io.micronaut.core.annotation.Introspected
  2. import javax.validation.constraints.Min
  3. import javax.validation.constraints.NotBlank
  4. @Introspected
  5. class Person {
  6. @NotBlank
  7. String name
  8. @Min(18L)
  9. int age
  10. }
  1. import io.micronaut.core.annotation.Introspected
  2. import javax.validation.constraints.Min
  3. import javax.validation.constraints.NotBlank
  4. @Introspected
  5. data class Person(
  6. @field:NotBlank var name: String,
  7. @field:Min(18) var age: Int
  8. )
The @Introspected annotation can be used as a meta-annotation; common annotations like @javax.persistence.Entity are treated as @Introspected

The above example defines a Person class that has two properties (name and age) that have constraints applied. Note that in Java the annotations can be on the field or the getter, and with Kotlin data classes, the annotation should target the field.

To validate the class manually, inject an instance of Validator:

  1. @Inject
  2. Validator validator;
  3. @Test
  4. void testThatPersonIsValidWithValidator() {
  5. Person person = new Person();
  6. person.setName("");
  7. person.setAge(10);
  8. final Set<ConstraintViolation<Person>> constraintViolations = validator.validate(person); (1)
  9. assertEquals(2, constraintViolations.size()); (2)
  10. }
  1. @Inject Validator validator
  2. void "test person is validated with validator"() {
  3. when:"The person is validated"
  4. def constraintViolations = validator.validate(new Person(name: "", age: 10)) (1)
  5. then:"A validation error occurs"
  6. constraintViolations.size() == 2 // (2)
  7. }
  1. @Inject
  2. lateinit var validator: Validator
  3. @Test
  4. fun testThatPersonIsValidWithValidator() {
  5. val person = Person("", 10)
  6. val constraintViolations = validator.validate(person) (1)
  7. assertEquals(2, constraintViolations.size) (2)
  8. }
1The validator validates the person
2The constraint violations are verified

Alternatively on Bean methods you can use javax.validation.Valid to trigger cascading validation:

  1. @Singleton
  2. public class PersonService {
  3. public void sayHello(@Valid Person person) {
  4. System.out.println("Hello " + person.getName());
  5. }
  6. }
  1. @Singleton
  2. class PersonService {
  3. void sayHello(@Valid Person person) {
  4. println "Hello $person.name"
  5. }
  6. }
  1. @Singleton
  2. open class PersonService {
  3. open fun sayHello(@Valid person: Person) {
  4. println("Hello ${person.name}")
  5. }
  6. }

The PersonService now validates the Person class when invoked:

  1. @Inject
  2. PersonService personService;
  3. @Test
  4. void testThatPersonIsValid() {
  5. Person person = new Person();
  6. person.setName("");
  7. person.setAge(10);
  8. final ConstraintViolationException exception =
  9. assertThrows(ConstraintViolationException.class, () ->
  10. personService.sayHello(person) (1)
  11. );
  12. assertEquals(2, exception.getConstraintViolations().size()); (2)
  13. }
  1. @Inject PersonService personService
  2. void "test person name is validated"() {
  3. when:"The sayHello method is called with an invalid person"
  4. personService.sayHello(new Person(name: "", age: 10)) (1)
  5. then:"A validation error occurs"
  6. def e = thrown(ConstraintViolationException)
  7. e.constraintViolations.size() == 2 // (2)
  8. }
  1. @Inject
  2. lateinit var personService: PersonService
  3. @Test
  4. fun testThatPersonIsValid() {
  5. val person = Person("", 10)
  6. val exception = assertThrows(ConstraintViolationException::class.java) {
  7. personService.sayHello(person) (1)
  8. }
  9. assertEquals(2, exception.constraintViolations.size) (2)
  10. }
1A validated method is invoked
2The constraint violations are verified

Validating Configuration Properties

You can also validate the properties of classes that are annotated with @ConfigurationProperties to ensure configuration is correct.

It is recommended that you annotate @ConfigurationProperties that features validation with @Context to ensure that the validation occurs at startup.

Defining Additional Constraints

To define additional constraints, create a new annotation, for example:

  1. import javax.validation.Constraint;
  2. import java.lang.annotation.Documented;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.Target;
  5. import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
  6. import static java.lang.annotation.ElementType.CONSTRUCTOR;
  7. import static java.lang.annotation.ElementType.FIELD;
  8. import static java.lang.annotation.ElementType.METHOD;
  9. import static java.lang.annotation.ElementType.PARAMETER;
  10. import static java.lang.annotation.ElementType.TYPE_USE;
  11. import static java.lang.annotation.RetentionPolicy.RUNTIME;
  12. @Retention(RUNTIME)
  13. @Constraint(validatedBy = { }) (1)
  14. public @interface DurationPattern {
  15. String message() default "invalid duration ({validatedValue})"; (2)
  16. /**
  17. * Defines several constraints on the same element.
  18. */
  19. @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
  20. @Retention(RUNTIME)
  21. @Documented
  22. @interface List {
  23. DurationPattern[] value(); (3)
  24. }
  25. }
  1. import javax.validation.Constraint
  2. import java.lang.annotation.Retention
  3. import static java.lang.annotation.RetentionPolicy.RUNTIME
  4. @Retention(RUNTIME)
  5. @Constraint(validatedBy = []) (1)
  6. @interface DurationPattern {
  7. String message() default "invalid duration ({validatedValue})" (2)
  8. }
  1. import javax.validation.Constraint
  2. import kotlin.annotation.AnnotationRetention.RUNTIME
  3. @Retention(RUNTIME)
  4. @Constraint(validatedBy = []) (1)
  5. annotation class DurationPattern(
  6. val message: String = "invalid duration ({validatedValue})" (2)
  7. )
1The annotation should be annotated with javax.validation.Constraint
2A message template can be provided in a hard-coded manner as above. If none is specified, Micronaut tries to find a message using ClassName.message using the MessageSource interface (optional)
3To support repeated annotations you can define an inner annotation (optional)
You can add messages and message bundles using the MessageSource and ResourceBundleMessageSource classes.

Once you have defined the annotation, implement a ConstraintValidator that validates the annotation. You can either create a bean class that implements the interface directly or define a factory that returns one or more validators.

The latter approach is recommended if you plan to define multiple validators:

  1. import io.micronaut.context.annotation.Factory;
  2. import io.micronaut.validation.validator.constraints.ConstraintValidator;
  3. import jakarta.inject.Singleton;
  4. @Factory
  5. public class MyValidatorFactory {
  6. @Singleton
  7. ConstraintValidator<DurationPattern, CharSequence> durationPatternValidator() {
  8. return (value, annotationMetadata, context) -> {
  9. context.messageTemplate("invalid duration ({validatedValue}), additional custom message"); (1)
  10. return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$");
  11. };
  12. }
  13. }
  1. import io.micronaut.context.annotation.Factory
  2. import io.micronaut.core.annotation.AnnotationValue
  3. import io.micronaut.validation.validator.constraints.ConstraintValidator
  4. import io.micronaut.validation.validator.constraints.ConstraintValidatorContext
  5. import jakarta.inject.Singleton
  6. @Factory
  7. class MyValidatorFactory {
  8. @Singleton
  9. ConstraintValidator<DurationPattern, CharSequence> durationPatternValidator() {
  10. return { CharSequence value,
  11. AnnotationValue<DurationPattern> annotation,
  12. ConstraintValidatorContext context ->
  13. context.messageTemplate("invalid duration ({validatedValue}), additional custom message") (1)
  14. return value == null || value.toString() ==~ /^PT?[\d]+[SMHD]{1}$/
  15. } as ConstraintValidator<DurationPattern, CharSequence>
  16. }
  17. }
  1. import io.micronaut.context.annotation.Factory
  2. import io.micronaut.validation.validator.constraints.ConstraintValidator
  3. import jakarta.inject.Singleton
  4. @Factory
  5. class MyValidatorFactory {
  6. @Singleton
  7. fun durationPatternValidator() : ConstraintValidator<DurationPattern, CharSequence> {
  8. return ConstraintValidator { value, annotation, context ->
  9. context.messageTemplate("invalid duration ({validatedValue}), additional custom message") (1)
  10. value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$".toRegex())
  11. }
  12. }
  13. }
1Override the default message template with an inline call for more control over the validation error message. (Since 2.5.0)

The above example implements a validator that validates any field, parameter etc. that is annotated with DurationPattern, ensuring that the string can be parsed with java.time.Duration.parse.

Generally null is regarded as valid and @NotNull is used to constrain a value as not being null. The example above regards null as a valid value.

For example:

  1. @Singleton
  2. public class HolidayService {
  3. public String startHoliday(@NotBlank String person,
  4. @DurationPattern String duration) {
  5. final Duration d = Duration.parse(duration);
  6. return "Person " + person + " is off on holiday for " + d.toMinutes() + " minutes";
  7. }
  8. }
  1. @Singleton
  2. class HolidayService {
  3. String startHoliday(@NotBlank String person,
  4. @DurationPattern String duration) {
  5. final Duration d = Duration.parse(duration)
  6. return "Person $person is off on holiday for ${d.toMinutes()} minutes"
  7. }
  8. }
  1. @Singleton
  2. open class HolidayService {
  3. open fun startHoliday(@NotBlank person: String,
  4. @DurationPattern duration: String): String {
  5. val d = Duration.parse(duration)
  6. return "Person $person is off on holiday for ${d.toMinutes()} minutes"
  7. }
  8. }

To verify the above examples validates the duration parameter, define a test:

  1. @Inject HolidayService holidayService;
  2. @Test
  3. void testCustomValidator() {
  4. final ConstraintViolationException exception =
  5. assertThrows(ConstraintViolationException.class, () ->
  6. holidayService.startHoliday("Fred", "junk") (1)
  7. );
  8. assertEquals("startHoliday.duration: invalid duration (junk), additional custom message", exception.getMessage()); (2)
  9. }
  1. void "test test custom validator"() {
  2. when:"A custom validator is used"
  3. holidayService.startHoliday("Fred", "junk") (1)
  4. then:"A validation error occurs"
  5. def e = thrown(ConstraintViolationException)
  6. e.message == "startHoliday.duration: invalid duration (junk), additional custom message" // (2)
  7. }
  1. @Inject
  2. lateinit var holidayService: HolidayService
  3. @Test
  4. fun testCustomValidator() {
  5. val exception = assertThrows(ConstraintViolationException::class.java) {
  6. holidayService.startHoliday("Fred", "junk") (1)
  7. }
  8. assertEquals("startHoliday.duration: invalid duration (junk), additional custom message", exception.message) (2)
  9. }
1A validated method is invoked
2THe constraint violations are verified

Validating Annotations at Compile Time

You can use Micronaut’s validator to validate annotation elements at compile time by including micronaut-validation in the annotation processor classpath:

  1. annotationProcessor("io.micronaut:micronaut-validation")
  1. <annotationProcessorPaths>
  2. <path>
  3. <groupId>io.micronaut</groupId>
  4. <artifactId>micronaut-validation</artifactId>
  5. </path>
  6. </annotationProcessorPaths>

Then Micronaut will at compile time validate annotation values that are themselves annotated with javax.validation. For example consider the following annotation:

  1. import java.lang.annotation.Retention;
  2. import static java.lang.annotation.RetentionPolicy.RUNTIME;
  3. @Retention(RUNTIME)
  4. public @interface TimeOff {
  5. @DurationPattern
  6. String duration();
  7. }
  1. import java.lang.annotation.Retention
  2. import static java.lang.annotation.RetentionPolicy.RUNTIME
  3. @Retention(RUNTIME)
  4. @interface TimeOff {
  5. @DurationPattern
  6. String duration()
  7. }
  1. import kotlin.annotation.AnnotationRetention.RUNTIME
  2. @Retention(RUNTIME)
  3. annotation class TimeOff(
  4. @DurationPattern val duration: String
  5. )

If you attempt to use @TimeOff(duration="junk") in your source, Micronaut will fail compilation due to the duration value violating the DurationPattern constraint.

If duration is a property placeholder such as @TimeOff(duration=”${my.value}”), validation is deferred until runtime.

Note that to use a custom ConstraintValidator at compile time you must instead define the validator as a class:

  1. import io.micronaut.core.annotation.NonNull;
  2. import io.micronaut.core.annotation.Nullable;
  3. import io.micronaut.core.annotation.AnnotationValue;
  4. import io.micronaut.validation.validator.constraints.ConstraintValidator;
  5. import io.micronaut.validation.validator.constraints.ConstraintValidatorContext;
  6. public class DurationPatternValidator implements ConstraintValidator<DurationPattern, CharSequence> {
  7. @Override
  8. public boolean isValid(
  9. @Nullable CharSequence value,
  10. @NonNull AnnotationValue<DurationPattern> annotationMetadata,
  11. @NonNull ConstraintValidatorContext context) {
  12. return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$");
  13. }
  14. }
  1. import io.micronaut.core.annotation.NonNull
  2. import io.micronaut.core.annotation.Nullable
  3. import io.micronaut.core.annotation.AnnotationValue
  4. import io.micronaut.validation.validator.constraints.ConstraintValidator
  5. import io.micronaut.validation.validator.constraints.ConstraintValidatorContext
  6. class DurationPatternValidator implements ConstraintValidator<DurationPattern, CharSequence> {
  7. @Override
  8. boolean isValid(
  9. @Nullable CharSequence value,
  10. @NonNull AnnotationValue<DurationPattern> annotationMetadata,
  11. @NonNull ConstraintValidatorContext context) {
  12. return value == null || value.toString() ==~ /^PT?[\d]+[SMHD]{1}$/
  13. }
  14. }
  1. import io.micronaut.core.annotation.AnnotationValue
  2. import io.micronaut.validation.validator.constraints.ConstraintValidator
  3. import io.micronaut.validation.validator.constraints.ConstraintValidatorContext
  4. class DurationPatternValidator : ConstraintValidator<DurationPattern, CharSequence> {
  5. override fun isValid(
  6. value: CharSequence?,
  7. annotationMetadata: AnnotationValue<DurationPattern>,
  8. context: ConstraintValidatorContext): Boolean {
  9. return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$".toRegex())
  10. }
  11. }

Additionally:

  • Define a META-INF/services/io.micronaut.validation.validator.constraints.ConstraintValidator file that references the class.

  • The class must be public and have a public no-argument constructor

  • The class must be on the annotation processor classpath of the project to be validated.