From 2a05125648967a68144cac89960b3504afd93214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Kobyli=C5=84ski?= Date: Wed, 29 Jul 2026 16:10:17 +0200 Subject: [PATCH 1/2] fix(spring-config): declare kotlin-reflect in okapi-spring-boot (#88) Spring binds this module's @ConfigurationProperties data classes through their Kotlin primary constructor, which it can only identify via Kotlin reflection: every parameter has a default, so kotlinc also emits a synthetic no-arg constructor, and Spring's value-object binding only applies when there is a single parameterized constructor. Without kotlin-reflect binding falls back to JavaBean binding and fails on the first okapi.* property with "No setter found" (the properties are all `val`). kotlin-reflect was reaching consumers only as a transitive of jackson-module-kotlin, declared by okapi-http and okapi-kafka alone. Any application implementing its own MessageDeliverer therefore failed to start as soon as it set any okapi.* property; the store modules do not help, since okapi-postgres lost that transitive in the plain-JDBC rewrite. Verified with a plain Java Spring Boot consumer (single DataSource, custom MessageDeliverer, okapi.processor.interval=PT2S): fails to start before this change, starts and applies the property after it. Claude-Session: https://claude.ai/code/session_013gbZvpKg73nNXtucDMY88d --- okapi-spring-boot/build.gradle.kts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/okapi-spring-boot/build.gradle.kts b/okapi-spring-boot/build.gradle.kts index e936339..df607e6 100644 --- a/okapi-spring-boot/build.gradle.kts +++ b/okapi-spring-boot/build.gradle.kts @@ -45,6 +45,17 @@ val springBootMajorForTests = ( dependencies { implementation(project(":okapi-core")) + // Required at runtime by every consumer, NOT optional. Spring binds the @ConfigurationProperties + // data classes in this module through their Kotlin PRIMARY constructor, which it can only + // identify via Kotlin reflection: all their parameters have defaults, so kotlinc also emits a + // synthetic no-arg constructor, and Spring's value-object binding only applies when there is a + // single parameterized constructor. Without kotlin-reflect it falls back to JavaBean binding and + // fails on the first okapi.* property with "No setter found" (the properties are all `val`). + // Do not drop this: today it also arrives transitively via jackson-module-kotlin, but only for + // consumers of okapi-http/okapi-kafka — okapi-postgres lost that transitive in the plain-JDBC + // rewrite. @ConstructorBinding is not a substitute (Kotlin copies it onto the no-arg ctor). + implementation(kotlin("reflect")) + compileOnly(libs.springContext) compileOnly(libs.springTx) compileOnly(libs.springJdbc) From 869934da2eb75f067076f9f88124b80cb4be8413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Kobyli=C5=84ski?= Date: Wed, 29 Jul 2026 16:14:50 +0200 Subject: [PATCH 2/2] docs(spring-config): trim the kotlin-reflect rationale comment The full story (transitive history via jackson-module-kotlin, the rejected @ConstructorBinding alternative) lives in #88 and the PR description; the build file only needs why the dependency is required and that it is not optional. Claude-Session: https://claude.ai/code/session_013gbZvpKg73nNXtucDMY88d --- okapi-spring-boot/build.gradle.kts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/okapi-spring-boot/build.gradle.kts b/okapi-spring-boot/build.gradle.kts index df607e6..5e705e5 100644 --- a/okapi-spring-boot/build.gradle.kts +++ b/okapi-spring-boot/build.gradle.kts @@ -45,15 +45,9 @@ val springBootMajorForTests = ( dependencies { implementation(project(":okapi-core")) - // Required at runtime by every consumer, NOT optional. Spring binds the @ConfigurationProperties - // data classes in this module through their Kotlin PRIMARY constructor, which it can only - // identify via Kotlin reflection: all their parameters have defaults, so kotlinc also emits a - // synthetic no-arg constructor, and Spring's value-object binding only applies when there is a - // single parameterized constructor. Without kotlin-reflect it falls back to JavaBean binding and - // fails on the first okapi.* property with "No setter found" (the properties are all `val`). - // Do not drop this: today it also arrives transitively via jackson-module-kotlin, but only for - // consumers of okapi-http/okapi-kafka — okapi-postgres lost that transitive in the plain-JDBC - // rewrite. @ConstructorBinding is not a substitute (Kotlin copies it onto the no-arg ctor). + // Runtime-required, not optional: Spring binds the @ConfigurationProperties data classes in this + // module via their Kotlin primary constructor, which it can only resolve through Kotlin + // reflection. Without it every okapi.* property fails to bind — see #88. implementation(kotlin("reflect")) compileOnly(libs.springContext)