3.15 Bean Validation
Since Micronaut 1.2, Micronaut has built-in support for validating beans that are annotated with javax.validation
annotations. As a minimum you should include the micronaut-validation
module as a compile dependency:
implementation("io.micronaut:micronaut-validation")
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-validation</artifactId>
</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 already computed compilation time metadata.
XML-based configuration
Instead of using
javax.validation.ConstraintValidator
you should use ConstraintValidator (io.micronaut.validation.validator.constraints.ConstraintValidator) to define custom constraints, which supports validating annotations at compilation 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 compilation time
Automatic compatibility with GraalVM native without additional configuration
If you require full Bean Validator 2.0 compliance you can add the micronaut-hibernate-validator
module to your classpath, which will replace Micronaut’s implementation.
implementation("io.micronaut.beanvalidation:micronaut-hibernate-validator")
<dependency>
<groupId>io.micronaut.beanvalidation</groupId>
<artifactId>micronaut-hibernate-validator</artifactId>
</dependency>
Validating Bean Methods
You can validate methods of any class declared as a Micronaut bean simply by using the javax.validation
annotation as arguments:
import javax.inject.Singleton;
import javax.validation.constraints.NotBlank;
@Singleton
public class PersonService {
public void sayHello(@NotBlank String name) {
System.out.println("Hello " + name);
}
}
import javax.inject.Singleton
import javax.validation.constraints.NotBlank
@Singleton
class PersonService {
void sayHello(@NotBlank String name) {
println "Hello $name"
}
}
import javax.inject.Singleton
import javax.validation.constraints.NotBlank
@Singleton
open class PersonService {
open fun sayHello(@NotBlank name: String) {
println("Hello $name")
}
}
The above example declares that the @NotBlank
annotation should be validated when executing the sayHello
method.
If you are using Kotlin the class and method must be declared open so that Micronaut can create a compilation 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. |
If a validation error occurs a javax.validation.ConstraintViolationException
will be thrown. For example:
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import javax.inject.Inject;
import javax.validation.ConstraintViolationException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@MicronautTest
class PersonServiceSpec {
@Inject PersonService personService;
@Test
void testThatNameIsValidated() {
final ConstraintViolationException exception =
assertThrows(ConstraintViolationException.class, () ->
personService.sayHello("") (1)
);
assertEquals("sayHello.name: must not be blank", exception.getMessage()); (2)
}
}
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification
import javax.inject.Inject
import javax.validation.ConstraintViolationException
@MicronautTest
class PersonServiceSpec extends Specification {
@Inject PersonService personService
void "test person name is validated"() {
when:"The sayHello method is called with a blank string"
personService.sayHello("") (1)
then:"A validation error occurs"
def e = thrown(ConstraintViolationException)
e.message == "sayHello.name: must not be blank" // (2)
}
}
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import javax.inject.Inject
import javax.validation.ConstraintViolationException
@MicronautTest
class PersonServiceSpec {
@Inject
lateinit var personService: PersonService
@Test
fun testThatNameIsValidated() {
val exception = assertThrows(ConstraintViolationException::class.java
) { personService.sayHello("") } (1)
assertEquals("sayHello.name: must not be blank", exception.message) (2)
}
}
1 | The method is called with a blank string |
2 | An exception occurs |
Validating Data Classes
If you wish to validate data classes, such as POJOs and so on, 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.
import io.micronaut.core.annotation.Introspected;
import javax.validation.constraints.*;
@Introspected
public class Person {
private String name;
@Min(18)
private int age;
@NotBlank
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
import io.micronaut.core.annotation.Introspected
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
@Introspected
class Person {
@NotBlank
String name
@Min(18l)
int age
}
import io.micronaut.core.annotation.Introspected
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
@Introspected
data class Person(
@field:NotBlank var name: String,
@field:Min(18) var age: Int
)
The @Introspected annotation can be used as a meta-annotation and common annotations like @javax.persistence.Entity are treated as @Introspected |
The above example defines a class called Person
that has 2 properties (name
and age
) that have constraints applied to them. 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.
If you wish to validate the class manually then you can inject an instance of Validator:
@Inject
Validator validator;
@Test
void testThatPersonIsValidWithValidator() {
Person person = new Person();
person.setName("");
person.setAge(10);
final Set<ConstraintViolation<Person>> constraintViolations = validator.validate(person); (1)
assertEquals(2, constraintViolations.size()); (2)
}
@Inject Validator validator
void "test person is validated with validator"() {
when:"The person is validated"
def constraintViolations = validator.validate(new Person(name: "", age: 10)) (1)
then:"A validation error occurs"
constraintViolations.size() == 2 // (2)
}
@Inject
lateinit var validator: Validator
@Test
fun testThatPersonIsValidWithValidator() {
val person = Person("", 10)
val constraintViolations = validator.validate(person) (1)
assertEquals(2, constraintViolations.size) (2)
}
1 | The validator is used to validate the person |
2 | The constraint violations are verified |
Alternatively on Bean methods you can use javax.validation.Valid
to trigger cascading validation:
@Singleton
public class PersonService {
public void sayHello(@Valid Person person) {
System.out.println("Hello " + person.getName());
}
}
@Singleton
class PersonService {
void sayHello(@Valid Person person) {
println "Hello $person.name"
}
}
@Singleton
open class PersonService {
open fun sayHello(@Valid person: Person) {
println("Hello ${person.name}")
}
}
The PersonService
will now validate the Person
class when invoked:
@Inject
PersonService personService;
@Test
void testThatPersonIsValid() {
Person person = new Person();
person.setName("");
person.setAge(10);
final ConstraintViolationException exception =
assertThrows(ConstraintViolationException.class, () ->
personService.sayHello(person) (1)
);
assertEquals(2, exception.getConstraintViolations().size()); (2)
}
@Inject PersonService personService
void "test person name is validated"() {
when:"The sayHello method is called with an invalid person"
personService.sayHello(new Person(name: "", age: 10)) (1)
then:"A validation error occurs"
def e = thrown(ConstraintViolationException)
e.constraintViolations.size() == 2 // (2)
}
@Inject
lateinit var personService: PersonService
@Test
fun testThatPersonIsValid() {
val person = Person("", 10)
val exception = assertThrows(ConstraintViolationException::class.java (1)
) { personService.sayHello(person) }
assertEquals(2, exception.constraintViolations.size) (2)
}
1 | A validated method is invoked |
2 | The 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 time. |
Defining Additional Constraints
To define additional constraints you can define a new annotation, for example:
import javax.validation.Constraint;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { }) (1)
public @interface DurationPattern {
String message() default "invalid duration ({validatedValue})"; (2)
/**
* Defines several constraints on the same element.
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
DurationPattern[] value(); (3)
}
}
import javax.validation.Constraint
import java.lang.annotation.*
import static java.lang.annotation.RetentionPolicy.RUNTIME
@Retention(RUNTIME)
@Constraint(validatedBy = []) (1)
@interface DurationPattern {
String message() default "invalid duration ({validatedValue})" (2)
}
import javax.validation.Constraint
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = []) (1)
annotation class DurationPattern(
val message: String = "invalid duration ({validatedValue})" (2)
)
1 | The annotation should be annotated with javax.validation.Constraint |
2 | A message template can be provided in a hard coded manner as above. If none is specified Micronaut will try to find a message using ClassName.message using the MessageSource interface. (optional) |
3 | To support repeated annotations you can define a inner annotation (optional). |
You can add messages and message bundles using the MessageSource and ResourceBundleMessageSource classes. |
Once you have defined the annotation you need to implement a ConstraintValidator that validates the annotation. You can either implement a bean 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:
import io.micronaut.context.annotation.Factory;
import io.micronaut.validation.validator.constraints.ConstraintValidator;
import javax.inject.Singleton;
@Factory
public class MyValidatorFactory {
@Singleton
ConstraintValidator<DurationPattern, CharSequence> durationPatternValidator() {
return (value, annotationMetadata, context) ->
value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$");
}
}
import io.micronaut.context.annotation.Factory
import io.micronaut.core.annotation.AnnotationValue
import io.micronaut.validation.validator.constraints.*
import javax.inject.Singleton
@Factory
class MyValidatorFactory {
@Singleton
ConstraintValidator<DurationPattern, CharSequence> durationPatternValidator() {
return { CharSequence value,
AnnotationValue<DurationPattern> annotation,
ConstraintValidatorContext context ->
return value == null || value.toString() ==~ /^PT?[\d]+[SMHD]{1}$/
} as ConstraintValidator<DurationPattern, CharSequence>
}
}
import io.micronaut.context.annotation.Factory
import io.micronaut.validation.validator.constraints.ConstraintValidator
import javax.inject.Singleton
@Factory
class MyValidatorFactory {
@Singleton
fun durationPatternValidator() : ConstraintValidator<DurationPattern, CharSequence> {
return ConstraintValidator { value, annotation, context ->
value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$".toRegex())
}
}
}
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 used to constrain a value as not being null . The example above regards null as a valid value. |
For example:
@Singleton
public class HolidayService {
public String startHoliday(@NotBlank String person,
@DurationPattern String duration) {
final Duration d = Duration.parse(duration);
return "Person " + person + " is off on holiday for " + d.toMinutes() + " minutes";
}
}
@Singleton
class HolidayService {
String startHoliday(@NotBlank String person,
@DurationPattern String duration) {
final Duration d = Duration.parse(duration)
return "Person $person is off on holiday for ${d.toMinutes()} minutes"
}
}
@Singleton
open class HolidayService {
open fun startHoliday( @NotBlank person: String,
@DurationPattern duration: String): String {
val d = Duration.parse(duration)
val mins = d.toMinutes()
return "Person $person is off on holiday for $mins minutes"
}
}
To verify the above examples validates the duration
parameter you can define a test:
@Inject HolidayService holidayService;
@Test
void testCustomValidator() {
final ConstraintViolationException exception =
assertThrows(ConstraintViolationException.class, () ->
holidayService.startHoliday("Fred", "junk") (1)
);
assertEquals("startHoliday.duration: invalid duration (junk)", exception.getMessage()); (2)
}
void "test test custom validator"() {
when:"A custom validator is used"
holidayService.startHoliday("Fred", "junk") (1)
then:"A validation error occurs"
def e = thrown(ConstraintViolationException)
e.message == "startHoliday.duration: invalid duration (junk)" // (2)
}
@Inject
lateinit var holidayService: HolidayService
@Test
fun testCustomValidator() {
val exception = assertThrows(ConstraintViolationException::class.java
) { holidayService.startHoliday("Fred", "junk") } (1)
assertEquals("startHoliday.duration: invalid duration (junk)", exception.message) (2)
}
1 | A validated method is invoked |
2 | THe constraint violations are verified |
Validating Annotations at Compilation Time
You can use Micronaut’s validator to validate annotation usages at compilation time. To do so you should include micronaut-validation
in the annotation processor classpath:
annotationProcessor("io.micronaut:micronaut-validation")
<annotationProcessorPaths>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-validation</artifactId>
</path>
</annotationProcessorPaths>
Once this is done Micronaut will at compilation validate annotation values that are themselves annotated with javax.validation
. For example consider the following annotation:
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeOff {
@DurationPattern
String duration();
}
import java.lang.annotation.*
@Retention(RetentionPolicy.RUNTIME)
@interface TimeOff {
@DurationPattern
String duration()
}
@Retention(AnnotationRetention.RUNTIME)
annotation class TimeOff(
@DurationPattern val duration: String
)
If your attempt to use @TimeOff(duration="junk")
in your source Micronaut will fail compilation due to the value of duration
violating the DurationPattern
constraint.
If duration is a property placeholder such as @TimeOff(duration=”${my.value}”) then validation handling will be deferred until runtime. |
Note that if you wish to allow use of a custom ConstraintValidator
at compilation time you should instead define the validator as a class:
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.validation.validator.constraints.*;
public class DurationPatternValidator implements ConstraintValidator<DurationPattern, CharSequence> {
@Override
public boolean isValid(
@Nullable CharSequence value,
@NonNull AnnotationValue<DurationPattern> annotationMetadata,
@NonNull ConstraintValidatorContext context) {
return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$");
}
}
import edu.umd.cs.findbugs.annotations.NonNull
import edu.umd.cs.findbugs.annotations.Nullable
import io.micronaut.core.annotation.AnnotationValue
import io.micronaut.validation.validator.constraints.*
class DurationPatternValidator implements ConstraintValidator<DurationPattern, CharSequence> {
@Override
boolean isValid(
@Nullable CharSequence value,
@NonNull AnnotationValue<DurationPattern> annotationMetadata,
@NonNull ConstraintValidatorContext context) {
return value == null || value.toString() ==~ /^PT?[\d]+[SMHD]{1}$/
}
}
import io.micronaut.core.annotation.AnnotationValue
import io.micronaut.validation.validator.constraints.*
class DurationPatternValidator : ConstraintValidator<DurationPattern, CharSequence> {
override fun isValid(
value: CharSequence?,
annotationMetadata: AnnotationValue<DurationPattern>,
context: ConstraintValidatorContext): Boolean {
return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$".toRegex())
}
}
In addition to the following requirements:
Define a
META-INF/services/io.micronaut.validation.validator.constraints.ConstraintValidator
file that references the class.The class should be public and feature a zero argument public constructor
The class should be placed on the annotation processor classpath of the project that is to be validated.