micronaut-projects / micronaut-projects/micronaut-validation

`@AssertTrue` on methods without `is`/`get` prefix silently ignored when using `@RequestBean` + `@Introspected` data class (KSP)

Open
#620 0 comments 0 reactions 0 assignees View on GitHub
status: awaiting validation type: bug
Dominant language
Java
Stars
9
Forks
8
Avg merge
20h 7m
Merged PRs (30d)
9

Description

### Description:

When using `@RequestBean` with a Kotlin data class annotated with `@Introspected`, `@AssertTrue` constraint on a method without `is`/`get` prefix is silently ignored — the method is never called by the validator. If the method is renamed to start with `is` or `get`, KSP fails at compile time with:

```
Primary Constructor or @Creator Method for Bindable property [checkEquality] found,
but there is no constructor/method parameter with name equal to [checkEquality].
```

### Steps to reproduce:

```kotlin
package com.example

import io.micronaut.core.annotation.Introspected
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces
import io.micronaut.http.annotation.RequestBean
import io.micronaut.runtime.Micronaut.run
import jakarta.validation.Valid
import jakarta.validation.constraints.AssertTrue
import jakarta.validation.constraints.Max
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank

/**
*
*/
fun main(args: Array) {
run(*args)
}

/**
*
*/
@Introspected
data class TestDto(
@field:Min(1)
@field:Max(10)
val id: Long,

@field:NotBlank
val text: String
) {

@AssertTrue
fun isCheckEquality() =
text == "$id"
}

/**
*
*/
@Controller("/test")
open class TestController {
@Get
@Produces(MediaType.TEXT_PLAIN)
open fun handle(
@Valid @RequestBean request: TestDto
): String {
println(request)
return "ok"
}
}
```

**build.gradle.kts**
```kotlin
plugins {
id("org.jetbrains.kotlin.jvm") version "2.3.20"
id("org.jetbrains.kotlin.plugin.allopen") version "2.3.20"
id("com.google.devtools.ksp") version "2.3.6"
id("io.micronaut.application") version "5.0.0-M1"
id("com.gradleup.shadow") version "9.4.1"
id("io.micronaut.aot") version "5.0.0-M1"
}

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

val kotlinVersion=project.properties.get("kotlinVersion")

repositories {
maven("https://central.sonatype.com/repository/maven-snapshots/") {
mavenContent { snapshotsOnly() }
}
mavenCentral()
maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") {
mavenContent { snapshotsOnly() }
}
}

dependencies {
compileOnly("io.micronaut.validation:micronaut-validation")
ksp("io.micronaut.validation:micronaut-validation-processor")

ksp("io.micronaut:micronaut-http-validation")
ksp("io.micronaut.serde:micronaut-serde-processor")
implementation("io.micronaut.kotlin:micronaut-kotlin-runtime")
implementation("io.micronaut.serde:micronaut-serde-jackson")
implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
compileOnly("io.micronaut:micronaut-http-client")
runtimeOnly("ch.qos.logback:logback-classic")
runtimeOnly("tools.jackson.module:jackson-module-kotlin")
testImplementation("io.micronaut:micronaut-http-client")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

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

java {
sourceCompatibility = 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
}

}

tasks.named("dockerfile") {

baseImage = "eclipse-temurin:25-jre"
}

// https://docs.gradle.org/current/userguide/upgrading_major_version_9.html#test_task_fails_when_no_tests_are_discovered
tasks.withType().configureEach {
failOnNoDiscoveredTests = false
}
```

### Environment Information

Micronaut: 5.0.0-SNAPSHOT
Kotlin: 2.3.20
KSP: 2.3.6
Java: 25.0.2 (Temurin)
micronaut-gradle-plugin: 5.0.0-M1

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the supplied Kotlin data class with @RequestBean, @Introspected, and @AssertTrue, then inspect the KSP-generated introspection and validation metadata. Done means an @AssertTrue method without an is/get prefix is invoked during request validation, while the prefixed form does not trigger the reported KSP constructor or bindable-property error.

Written by the indexing model from the issue text.

Assessment

Tech stack
kotlin
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.