Micronaut body parameter validation ignores jakarta annotations
10:57 27 Aug 2026

I got stuck on jakarta annotations not being enforced. I managed to find only one good reference (Validation not working, DTO not getting validated) and was left no better for finding it. I would think that @Introspected and @Validated and @Valid annotations would force validation, which would happen before request handling in the method happens, but that doesn't not seem to be the case. POST body is bound to the object with invalid value (null for first name) and request is handled regardless of that, which ultimately comes back with 200 OK instead of expected by me 400 BAD_REQUEST.

build.gradle:

plugins {
    id("io.micronaut.application") version "5.0.2"
    id("com.gradleup.shadow") version "9.4.1"
    id("io.micronaut.aot") version "5.0.2"
}

version = "0.1"
group = "com.example"



repositories {
    mavenCentral()
}

dependencies {
    def jakartaVer='3.0.0'


    runtimeOnly("io.micronaut:micronaut-management:5.1.0")

    implementation group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: jakartaVer
    implementation group: 'jakarta.validation', name: 'jakarta.validation-api', version: jakartaVer
    compileOnly group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: '6.1.0'
    implementation("io.micronaut.data:micronaut-data-hibernate-jpa:5.0.6")
    implementation("io.micronaut.data:micronaut-data-tx-hibernate")
    implementation("io.micronaut.sql:micronaut-jdbc-hikari")
    runtimeOnly("io.micronaut.data:micronaut-data-jdbc:5.0.6")
    runtimeOnly("org.postgresql:postgresql")
    compileOnly("jakarta.persistence:jakarta.persistence-api")

    implementation("tools.jackson.core:jackson-core:3.2.1")


    annotationProcessor("io.micronaut.validation:micronaut-validation-processor")
    annotationProcessor("io.micronaut:micronaut-http-validation")
    annotationProcessor("io.micronaut.serde:micronaut-serde-processor")
    implementation("io.micronaut.beanvalidation:micronaut-hibernate-validator")
    implementation("io.micronaut.validation:micronaut-validation")
    implementation("io.micronaut.serde:micronaut-serde-jackson")
    compileOnly("io.micronaut:micronaut-http-client")
    runtimeOnly("ch.qos.logback:logback-classic")
    runtimeOnly("org.yaml:snakeyaml")

    testImplementation("io.micronaut:micronaut-http-client")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
    testImplementation("io.micronaut.test:micronaut-test-junit5:5.0.1")
    testImplementation("org.mockito:mockito-junit-jupiter:5.23.0")
    testImplementation("org.mockito:mockito-core:5.23.0")
    testImplementation("org.assertj:assertj-core:3.27.7")

}

application {
    mainClass = "com.example.Application"
}

java {
    sourceCompatibility = JavaVersion.toVersion("25")
    targetCompatibility = JavaVersion.toVersion("25")
}

graalvmNative.toolchainDetection = false
graalvmNative {
    binaries {
        all {
            buildArgs.add("-H:+SharedArenaSupport")
        }
    }
}

micronaut {
    runtime("netty")
    testRuntime("junit5")
    processing {
        incremental(true)
        annotations("com.example.*")
    }
    aot {
        // Please review carefully the optimizations enabled below
        // Check https://micronaut-projects.github.io/micronaut-aot/latest/guide/ for more details
        optimizeServiceLoading = false
        convertYamlToJava = false
        precomputeOperations = true
        cacheEnvironment = true
        optimizeClassLoading = true
        deduceEnvironment = true
        optimizeNetty = true
        replaceLogbackXml = true
    }

}

Model class:

@Introspected // afaik redundant due to marking as @Entity
@Serdeable
@Entity //transitively also @Introspected
@Table(name = "contact_details")
public class ContactDetails {

    public ContactDetails() {

    }

    public ContactDetails(Long uuid, String firstName, String lastName, String email, String phoneNo) {
        this.uuid = uuid;
        this. firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.phoneNo = phoneNo;
    }

    @Id
    @GeneratedValue
    private Long uuid;

    @NotBlank
    private String firstName;
    @NotBlank
    private String lastName;
    @NotBlank
    @Email
    @Column(unique = true)
    private String email;
    @Digits(integer = 15, fraction = 0)
    private String phoneNo;

// getters and setters for each field
}

Controller:

@Validated
@Controller
public class ContactController {

    @Inject
    private Validator validator;

    private final ContactService contactService;

    public ContactController(ContactService contactService) {
        this.contactService = contactService;
    }
(...)
    @Validated
    @Post(uri = "/contact")
    @Produces(MediaType.APPLICATION_JSON)
    public MutableHttpResponse postContact(@Body @Valid ContactDetails contactDetails) throws URISyntaxException {
(...)
        return HttpResponse.created(new URI("/contacts/" + savedContact.getUuid().toString())).body(savedContact);
    }

(...)
}

Which I tried to test with:

@MicronautTest
public class ContactControllerTest {

    @Inject
    @Client("/")
    HttpClient client;

    @Inject
    ContactService contactService;

    @Inject
    private Validator validator;

(...)
    //TODO: jakarta annotation doesn't seem to work
    @Test
    void whenPostEmptyName_thenReturn400() throws Exception {
        // first name set to null in order to trip over not blank constraint in controller
        ContactDetails requestBody = new ContactDetails(0L, null, "Smith", "ts@example.com", "079678234");


        HttpRequest request = HttpRequest.POST("/contact", requestBody).accept(MediaType.APPLICATION_JSON);
        HttpResponse response = client.toBlocking().exchange(request);

        assertEquals(HttpStatus.BAD_REQUEST, response.getStatus());
    }
    
}

Sadly validation doesn't seem to be applied. I tried setting the constraints on setters and constructor params, again - no luck.

I just gave Micronaut a try, I have no idea i.e. how the two constructors affect the way things are composed but I assume that annotating field, constructor params and setters should do the trick.

PS: I tried to use validator separately and that worked, narrowing down possible issues to jakarta not being used to validate the input rather than validators doing wrong things. Sample test attached below:

    @Inject
    @Client("/")
    HttpClient client;

    @Inject
    ContactService contactService;

    @Inject
    Validator validator;
(...)    

@Test
    void whenPostEmptyName_thenReturn400() throws Exception {
        // first name set to null in order to trip over not blank constraint in controller
        ContactDetails requestBody = new ContactDetails(0L, null, "Smith", "ts@example.com", "079678234");


        final Set> constraintViolations = validator.validate(requestBody);  // (1)

        assertEquals(1, constraintViolations.size()); // correctly spots violation of non-blank rule

    }
java micronaut jakarta-validation