Skip to content

Changelog

Semantic HTTP responses for deserialization errors

Section titled “Semantic HTTP responses for deserialization errors”

In previous versions of Kairo, deserialization errors resulted in an HTTP 400 response with no response body. Now, deserialization errors are now mapped to semantic HTTP responses.

Suppose you have this simple data class.

data class Rep(val value: Int)

Here are some example request/response pairs.

POST {}
HTTP 400 Bad Request
{
"type": "MissingProperty",
"status": 400,
"message": "Missing property",
"detail": null,
"path": "/value"
}
POST {"value":"Hello, World!"}
HTTP 400 Bad Request
{
"type": "InvalidProperty",
"status": 400,
"message": "Invalid property",
"detail": null,
"path": "/value"
}
POST {"value":42,"other":"Hello, World!"}
HTTP 400 Bad Request
{
"type": "UnrecognizedProperty",
"status": 400,
"message": "Unrecognized property",
"detail": null,
"path": "/other"
}

Introduction of kairo-validation with common validation patterns.

Validator.emailAddress.matches("jeff@example.com") // true
Validator.emailAddress.matches("not-an-email") // false

In addition to the default Money serialization format, Kairo now supports custom serialization formats.

// Default format
val json: KairoJson =
KairoJson {
addModule(MoneyModule())
}
json.serialize(Money.of("123.45", "USD"))
// => {"amount":123.45,"currency":"USD"}
// Custom format
object CustomMoneyFormat : MoneyFormat() {
override val serializer: JsonSerializer<Money> =
object : StdSerializer<Money>(Money::class.java) {
override fun serialize(
value: Money,
gen: JsonGenerator,
provider: SerializerProvider,
) {
// Your implementation.
}
}
override val deserializer: JsonDeserializer<Money> =
object : StdDeserializer<Money>(Money::class.java) {
override fun deserialize(
p: JsonParser,
ctxt: DeserializationContext,
): Money {
// Your implementation.
}
}
}
val json: KairoJson =
KairoJson {
addModule(
MoneyModule {
moneyFormat = CustomMoneyFormat
}
)
}
json.serialize(Money.of("123.45", "USD"))
  • Upgrade Kotlin from 2.3.0 to 2.3.10: See Kotlin’s release notes here.

  • Explicit backing fields: Kotlin 2.3 introduces explicit backing fields, which Kairo has adopted. We recommend enabling -Xexplicit-backing-fields so you can use them too.

  • Upgrade Detekt from 2.0.0-alpha.1 to 2.0.0-alpha.2: See Detekt’s release notes here.

  • Upgrade Slack SDK from 1.46.0 to 1.47.0: See Slack SDK’s release notes here.

Fixed type erasure during REST (de)serialization

Section titled “Fixed type erasure during REST (de)serialization”

Kairo already uses a Jackson wrapper in order to avoid runtime JVM type erasure issues. However, Ktor’s JacksonConverter was bypassing this within some of the REST code (both client and server), leading to incorrect (de)serialization in some instances.

To fix this, a custom Ktor ContentConverter called KairoConverter was introduced, which replace Ktor’s JacksonConverter and is used by default.

Kairo 6.0 is a major release, with several breaking changes.

Kairo’s goal has always been to offer a set of libraries that work well together yet remain flexible and modular. Previous versions of Kairo, however, fell short. Libraries were too primitive or bespoke, or were too tightly bound to specific design decisions.

Kairo 6 is a complete reset: Every library has been completely reworked, now truly living up to these ideals.

Several libraries have been simplified and many others have been eliminated entirely, in favor of best-in-class external libraries that do the job far better.

With Kairo 6, there’s also now a bias toward Kotlin-first libraries instead of Java ones, reflecting the ecosystem’s maturity since Kairo started in 2019. That said, Kairo 6 still uses Jackson, and has not migrated to kotlinx.serialization at this time.

  • Improved documentation. Every library now has examples and testing guidance, reducing friction to adopt. A documentation website and getting started guide have also been created.

  • BOMs for dependency alignment. Use software.airborne.kairo:bom for standalone libraries, or software.airborne.kairo:bom-full for Kairo applications. Keeps both Kairo and key external libraries in sync automatically.

  • Dependency injection with Koin (replaces Guice). Reflection-free, Kotlin-friendly, better tooling, and simpler to configure.

  • Easier REST definition & routing. Some custom Kairo syntax has been reverted to Ktor native routing syntax plus optional DSL helpers.

  • Type-safe SQL using Exposed’s DSL (replaces JDBI).

    • No more manual SQL strings, but retaining similar semantic alignment for predictability and easier debugging.
  • Switch to HOCON for industry-standard configs.

    • Great developer ergonomics (comments, human-readable syntax, less boilerplate).
    • Built-in config inheritance and overrides for easy multi-env (dev/staging/prod).
    • Native environment variable substitution.
  • Safer IDs with zero runtime cost. Kairo IDs like user_ccU4Rn4DKVjCMqt3d0oAw3 now have compile-time safety, meaning you can’t mix up a user ID and a business ID. This is done without runtime overhead.

  • Simpler and faster integration testing.

    • No need to spin up Ktor anymore — test the service layer directly.
    • Tests also run in parallel now!
  • Upgrade Gradle from 8 to 9.

  • Upgrade Kotlin from 2.2 to 2.3.

  • Introduction of kairo-application lets you start your Server, wait for JVM termination, and clean up afterwards, all with a single call.

No changes.

  • Switch to HOCON for industry-standard configs.

    • Great developer ergonomics (comments, human-readable syntax, less boilerplate).
    • Built-in config inheritance and overrides for easy multi-env (dev/staging/prod).
    • Native environment variable substitution.
  • Introduction of config resolvers, which let you pull in properties from other sources like Google Secret Manager.

  • singleNullOrThrow() now works with Kotlin Flow.
  • Introduction of emitAll() for Iterable.

No changes.

  • Dependency injection with Koin (replaces Guice). Reflection-free, Kotlin-friendly, better tooling, and simpler to configure.
  • Introduction of logical failures to describe situations not deemed successful in your domain, but still within the realms of that domain.
    • JSON serialization of logical failures.
    • Easily testable.
  • Features now start and stop in parallel, improving Server performance.

  • Feature lifecycle handlers have been refactored.

  • GCP secrets are now fetched asynchronously on coroutines. No blocking threads, and you can fetch multiple in parallel

  • FakeGcpSecretSupplier for easier testing.

  • Google App Engine is no longer supported. For apps previously running on Google App Engine, consider using Google Cloud Run instead.
  • Health checks now reflect actual readiness. They won’t pass until Ktor can serve traffic.

  • Health checks now run in parallel.

  • Health checks now have timeouts.

  • Introduction of kairo-hocon, which supports kairo-config.
  • Safer IDs with zero runtime cost. Kairo IDs like user_ccU4Rn4DKVjCMqt3d0oAw3 now have compile-time safety, meaning you can’t mix up a user ID and a business ID. This is done without runtime overhead.

  • The valid entropy range has been expanded.

  • Testing now uses random IDs instead of deterministic IDs. Deterministic IDs are no longer supported by default.

  • Applications no longer need to install IdFeature in order to generate IDs.

  • Introduction of kairo-image (some utilities for working with images).
  • Simpler and faster integration testing.

    • No need to spin up Ktor anymore — test the service layer directly.
    • Tests also run in parallel now!
  • Integration tests now use JUnit extensions instead of inheritance.

  • No longer tightly coupled to Log4j2. Choose your own SJF4J logging backend!

    • Recommended: Stable Log4j 2 instead of beta Log4j 3.
    • Simplified recommended local log format (does not affect GCP logs).
  • Guidance to reduce noisy logs in production.

  • Introduction of kairo-mailersend, letting you easily send emails through MailerSend.

No changes.

  • Introduced Optional differentiate between missing and null properties.

  • Removed Updater and update in favor of Optional.

  • Minor changes to toString() result of ProtectedString.
  • Added support for nullable KairoTypes.
  • Easier REST definition & routing. Some custom Kairo syntax has been reverted to Ktor native routing syntax plus optional DSL helpers.

  • Switch from CIO to Netty. Netty’s performance far exceeds CIOs in most situations, including with coroutines. Netty is also a far more popular library than CIO. This change was actually made back in Kairo 5.0.

  • Added support for list query params.

  • The @RestEndpoint.ContentType and @RestEndpoint.Accept annotations are now optional.

  • List query params are now supported.

  • No more REST context class. Access Ktor’s RoutingCall directly.

  • Native Ktor SSE support.

  • Better error messages when RestEndpoints are malformed — easier debugging.

  • Colored call logging when running locally. Instantly spot failures!

  • Automatic string trimming removed. Data is now preserved exactly as sent.

  • Native support for Ktor types (HttpMethod and HttpStatusCode).

  • Ability to customize several default serialization formats.

No changes.

  • Now uses Slack’s AsyncMethodsClient directly.
  • Type-safe SQL using Exposed’s DSL (replaces JDBI).

    • No more manual SQL strings, but retaining similar semantic alignment for predictability and easier debugging.
  • R2DBC driver for async I/O (replaces JDBC).

  • SQL health checks no longer run queries, avoiding DB log pollution.

  • Custom type handling has been removed.

  • Custom transaction management has been removed.

  • Introduction of kairo-stytch, letting you easily manage identity through Stytch.
  • Upgrade from Kotest 5 to Kotest 6.

  • Test helper method descriptions are now optional.

  • Removal of kairoEquals, kairoHashCode and kairoToString.

  • Addition of canonicalize() and slugify().

  • Addition of firstCauseOf<T>() for exceptions.

  • Addition of the resource() Guava wrapper.

  • Alternative Money Formatters
  • Clock
  • Closeable (use built-in closeables instead).
  • Command Runner. Connecting to GCP SQL instances that use IAM Authentication is now supported through kairo-sql-gcp
  • Date Range
  • Do Not Log String
  • Environment Variable Supplier. HOCON configs support native environment variable substitution.
  • Google Common
  • Google Cloud Scheduler
  • Google Cloud Tasks
  • Hashing
  • Lazy Supplier
  • MDC
  • Time
  • Transaction Manager
  • UUID (use Kotlin’s Uuid class directly instead).