Skip to content

fix(spring-config): declare kotlin-reflect in okapi-spring-boot - #89

Merged
rawo merged 2 commits into
mainfrom
fix/kotlin-reflect-config-binding
Jul 29, 2026
Merged

fix(spring-config): declare kotlin-reflect in okapi-spring-boot#89
rawo merged 2 commits into
mainfrom
fix/kotlin-reflect-config-binding

Conversation

@endrju19

Copy link
Copy Markdown
Collaborator

Fixes #88.

okapi-spring-boot binds its @ConfigurationProperties classes through the Kotlin primary constructor, which Spring can only identify via Kotlin reflection. No okapi module declared kotlin-reflect, so whether a consumer application starts depended on whether an unrelated dependency happened to drag it onto the runtime classpath.

Why the primary constructor cannot be resolved without it

All four properties classes (OkapiProperties + nested Liquibase, OkapiMetricsProperties, OutboxProcessorProperties, OutboxPurgerProperties) are data classes whose parameters all have defaults, so kotlinc also emits a synthetic no-arg constructor:

OkapiProperties(String, String, Liquibase)
OkapiProperties(String, String, Liquibase, int, DefaultConstructorMarker)
OkapiProperties()

Spring Boot uses value-object binding only when there is a single parameterized constructor; otherwise it must pick the Kotlin primary constructor. Without kotlin-reflect it falls back to JavaBean binding, finds no setters (all val), and fails.

Who was affected

kotlin-reflect reached consumers only as a transitive of jackson-module-kotlin, declared by okapi-http and okapi-kafka alone. Any application using its own MessageDeliverer was affected — the store modules do not help, since okapi-postgres lost that transitive in the plain-JDBC rewrite (it still had it in 0.1.0/0.3.0). Java consumers are the most exposed, having no route by which kotlin-reflect could arrive by accident.

Verification

A plain Java Spring Boot service (single DataSource, custom MessageDeliverer, okapi-core + okapi-spring-boot + okapi-postgres, one ordinary okapi.processor.interval: PT2S):

  • before: APPLICATION FAILED TO STARTNo setter found for property: interval
  • after: starts, and the property is applied: Outbox processor started [interval=PT2S, batchSize=10, concurrency=1]

Also run on this branch: ktlintFormat, full ktlintCheck, :okapi-spring-boot:test — all green. The generated POM now carries kotlin-reflect at runtime scope, so consumers get it transitively without declaring anything.

Note on test coverage

This cannot be covered by a test in this repository: kotlin-reflect is on the test runtime classpath of every module (transitively, via Kotest), so property-binding tests pass with or without the fix. That is precisely why it went unnoticed. The failure is only observable from a consumer build, which is how it was reproduced and verified above. Happy to add a build-level guard asserting the dependency stays on okapi-spring-boot's runtime classpath if you would like one — say the word and I will push it.

Rejected alternative

Annotating the primary constructor with @ConstructorBinding does not work: without kotlin-reflect Spring cannot tell the constructors apart, Kotlin copies the annotation onto the synthetic no-arg constructor, and startup fails earlier with declares @ConstructorBinding on a no-args constructor. Tested.

https://claude.ai/code/session_013gbZvpKg73nNXtucDMY88d

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
Copilot AI review requested due to automatic review settings July 29, 2026 14:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request fixes Spring Boot @ConfigurationProperties binding failures in consumer applications by ensuring kotlin-reflect is present on the runtime classpath when using okapi-spring-boot.

Changes:

  • Declare kotlin-reflect as a dependency of okapi-spring-boot so Spring can resolve Kotlin primary constructors during configuration-properties binding.
  • Add in-file rationale documenting why the dependency is required and why alternatives (like @ConstructorBinding) don’t address the underlying problem.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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
Copilot AI review requested due to automatic review settings July 29, 2026 14:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

@rawo rawo self-assigned this Jul 29, 2026
@rawo
rawo merged commit 9e9c8be into main Jul 29, 2026
9 checks passed
@rawo
rawo deleted the fix/kotlin-reflect-config-binding branch July 29, 2026 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

okapi-spring-boot fails to bind any okapi.* property when kotlin-reflect is absent from the consumer classpath

3 participants