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
}