Changelog
Kairo 6.1.0
Section titled “Kairo 6.1.0”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"}Validation
Section titled “Validation”Introduction of kairo-validation with common validation patterns.
Validator.emailAddress.matches("jeff@example.com") // trueValidator.emailAddress.matches("not-an-email") // falseMoney serialization improvements
Section titled “Money serialization improvements”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"))Other changes
Section titled “Other changes”-
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-fieldsso 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.
Kairo 6.0.1
Section titled “Kairo 6.0.1”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.0
Section titled “Kairo 6.0.0”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.
Kairo 6.0 highlights
Section titled “Kairo 6.0 highlights”-
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:bomfor standalone libraries, orsoftware.airborne.kairo:bom-fullfor 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_ccU4Rn4DKVjCMqt3d0oAw3now 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!
Full Kairo 6.0 changelog
Section titled “Full Kairo 6.0 changelog”Overall
Section titled “Overall”-
Upgrade Gradle from 8 to 9.
-
Upgrade Kotlin from 2.2 to 2.3.
Application
Section titled “Application”- Introduction of
kairo-applicationlets you start your Server, wait for JVM termination, and clean up afterwards, all with a single call.
Client
Section titled “Client”No changes.
Config
Section titled “Config”-
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.
Coroutines
Section titled “Coroutines”singleNullOrThrow()now works with KotlinFlow.- Introduction of
emitAll()forIterable.
No changes.
Datetime
Section titled “Datetime”- Introduced kotlinx-datetime.
Dependency Injection
Section titled “Dependency Injection”- Dependency injection with Koin (replaces Guice). Reflection-free, Kotlin-friendly, better tooling, and simpler to configure.
Exception
Section titled “Exception”- 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.
Feature
Section titled “Feature”-
Features now start and stop in parallel, improving Server performance.
-
Feature lifecycle handlers have been refactored.
GCP Secret Supplier
Section titled “GCP Secret Supplier”-
GCP secrets are now fetched asynchronously on coroutines. No blocking threads, and you can fetch multiple in parallel
-
FakeGcpSecretSupplierfor easier testing.
Google App Engine
Section titled “Google App Engine”- Google App Engine is no longer supported. For apps previously running on Google App Engine, consider using Google Cloud Run instead.
Health Check
Section titled “Health Check”-
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 supportskairo-config.
-
Safer IDs with zero runtime cost. Kairo IDs like
user_ccU4Rn4DKVjCMqt3d0oAw3now 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
IdFeaturein order to generate IDs.
- Introduction of
kairo-image(some utilities for working with images).
Integration Testing
Section titled “Integration Testing”-
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.
Logging
Section titled “Logging”-
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.
Mailersend
Section titled “Mailersend”- Introduction of
kairo-mailersend, letting you easily send emails through MailerSend.
No changes.
Optional
Section titled “Optional”-
Introduced
Optionaldifferentiate between missing and null properties. -
Removed
Updaterandupdatein favor ofOptional.
Protected String
Section titled “Protected String”- Minor changes to
toString()result ofProtectedString.
Reflect
Section titled “Reflect”- 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.ContentTypeand@RestEndpoint.Acceptannotations are now optional. -
List query params are now supported.
-
No more REST context class. Access Ktor’s
RoutingCalldirectly. -
Native Ktor SSE support.
-
Better error messages when
RestEndpoints are malformed — easier debugging. -
Colored call logging when running locally. Instantly spot failures!
Serialization
Section titled “Serialization”-
Automatic string trimming removed. Data is now preserved exactly as sent.
-
Native support for Ktor types (
HttpMethodandHttpStatusCode). -
Ability to customize several default serialization formats.
Server
Section titled “Server”No changes.
- Now uses Slack’s
AsyncMethodsClientdirectly.
-
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.
Stytch
Section titled “Stytch”- Introduction of
kairo-stytch, letting you easily manage identity through Stytch.
Testing
Section titled “Testing”-
Upgrade from Kotest 5 to Kotest 6.
-
Test helper method descriptions are now optional.
-
Removal of
kairoEquals,kairoHashCodeandkairoToString. -
Addition of
canonicalize()andslugify(). -
Addition of
firstCauseOf<T>()for exceptions. -
Addition of the
resource()Guava wrapper.
Removed libraries
Section titled “Removed libraries”- 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
Uuidclass directly instead).