Make JSON Faster

http4k Core

The standard JSON format modules included in Http4k (like Jackson and Moshi) are good enough for most applications. However there are scenarios where you may want to further optimize them for runtime performance, or to achieve acceptable cold start times in serverless deployments.

kotlinx.serialization

kotlinx.serialization is a relatively simple and effective option. It’s fully supported by Http4k, but we’ll go over it first as a baseline to compare against the others. It generates the serialization adapters at compile time, which means it doesn’t need reflection during runtime.

kotlinx.serialization is a very fast and efficient module, but there are some caveats to consider:

  1. No support for many builtin JVM types (e.g. java.time, UUID)
  2. Custom serializers must be registered via annotation
  3. No support for values4k classes

Gradle

plugins {
    kotlin("plugin.serialization") version "<kotlin version>"
}

dependencies {
    
    implementation(platform("org.http4k:http4k-bom:5.33.1.0"))

    implementation("org.http4k:http4k-format-kotlinx-serialization")
}

Example

package content.howto.make_json_faster

import kotlinx.serialization.Serializable
import org.http4k.core.Method
import org.http4k.core.Request
import org.http4k.core.with
import org.http4k.format.KotlinxSerialization
import java.util.UUID

// You must annotate your class to generate the adapter
@Serializable
data class KotlinXCat(
    val id: String, // UUID not supported
    val name: String
)

// use the builtin http4k module
private val json = KotlinxSerialization

fun main() {
    val cat = KotlinXCat(UUID.randomUUID().toString(), "Kratos")

    // serialize
    val string = json.asFormatString(cat)
        .also(::println)

    // deserialize
    json.asA<KotlinXCat>(string)
        .also(::println)

    // make a lens
    val lens = json.autoBody<KotlinXCat>().toLens()
    Request(Method.GET, "foo").with(lens of cat)
}

Moshi Metadata Reflect

moshi-metadata-reflect is a plugin for Moshi that replaces the heavyweight kotlin-reflect with the lighter kotlinx-metadata-jvm. While this module still uses reflection, eliminating kotlin-reflect from your classpath will reduce your binary size by several MB and make a respectable improvement in cold start time.

Moshi Metadata Reflect is very simple, and doesn’t require any gradle plugins or finicky code generation. However, it will bring the smallest performance benefit out of these options.

Gradle

dependencies {
    
    implementation(platform("org.http4k:http4k-bom:5.33.1.0"))

    implementation("org.http4k:http4k-format-moshi") {
        exclude("org.jetbrains.kotlin", "kotlin-reflect") // Exclude kotlin-reflect
    }
    implementation("dev.zacsweers.moshix:moshi-metadata-reflect:<moshi metadata version>")
}

Example

package content.howto.make_json_faster

import com.squareup.moshi.Moshi
import dev.zacsweers.moshix.reflect.MetadataKotlinJsonAdapterFactory
import org.http4k.core.Method
import org.http4k.core.Request
import org.http4k.core.with
import org.http4k.format.ConfigurableMoshi
import org.http4k.format.EventAdapter
import org.http4k.format.ListAdapter
import org.http4k.format.MapAdapter
import org.http4k.format.ThrowableAdapter
import org.http4k.format.asConfigurable
import org.http4k.format.withStandardMappings
import java.util.UUID

private val json = ConfigurableMoshi(
    Moshi.Builder()
        .addLast(EventAdapter)
        .addLast(ThrowableAdapter)
        .addLast(ListAdapter)
        .addLast(MapAdapter)
        .asConfigurable(MetadataKotlinJsonAdapterFactory()) // <-- moshi-metadata-reflect
        .withStandardMappings()
        .done()
)

data class MoshiCat(val id: UUID, val name: String)

fun main() {
    val cat = MoshiCat(UUID.randomUUID(), "Kratos")

    // serialize
    val string = json.asFormatString(cat)
        .also(::println)

    // deserialize
    json.asA<MoshiCat>(string)
        .also(::println)

    // make a lens
    val lens = json.autoBody<MoshiCat>().toLens()
    Request(Method.GET, "foo").with(lens of cat)
}

Kotshi

Kotshi is a plugin for Moshi. It’s very similar to moshi-kotlin-codegen in that they both generate Moshi adapters at compile time. However, since moshi-kotlin-codegen still requires kotlin-reflect at runtime, kotshi can bring much greater cold start performance gains.

Kotshi is a very fast and efficient module, but is the most difficult to set up. It requires a gradle plugin, several dependencies, and depends on code that only exists after compile.

Gradle

plugins {
    // The KSP plugin is required to generate the adapters at compile time
    id("com.google.devtools.ksp") version "<ksp plugin version version>"
}

dependencies {
    implementation("org.http4k:http4k-format-moshi") {
        exclude("org.jetbrains.kotlin", "kotlin-reflect") // Exclude kotlin-reflect
    }
    
    // Get the Kotshi runtime library and configure KSP to generate the adapters
    implementation("se.ansman.kotshi:api:<kotshi version>")
    ksp("se.ansman.kotshi:compiler:<kotshi version>")
}

Example

//package content.howto.make_json_faster
//
//import com.squareup.moshi.JsonAdapter
//import com.squareup.moshi.Moshi
//import org.http4k.core.Method
//import org.http4k.core.Request
//import org.http4k.core.with
//import org.http4k.format.ConfigurableMoshi
//import org.http4k.format.EventAdapter
//import org.http4k.format.ListAdapter
//import org.http4k.format.MapAdapter
//import org.http4k.format.ThrowableAdapter
//import org.http4k.format.asConfigurable
//import org.http4k.format.withStandardMappings
//import se.ansman.kotshi.JsonSerializable
//import se.ansman.kotshi.KotshiJsonAdapterFactory
//import java.util.UUID
//
//// Annotation is required to generate the adapter
//@JsonSerializable
//data class KotshiCat(val id: UUID, val name: String)
//
//// Build the kotshi adapter
//@KotshiJsonAdapterFactory
//private object ExampleJsonAdapterFactory : JsonAdapter.Factory by
//    KotshiExampleJsonAdapterFactory // this class will be generated during compile
//
//private val json = ConfigurableMoshi(
//    Moshi.Builder()
//        .add(ExampleJsonAdapterFactory) // inject kotshi here
//        .addLast(EventAdapter)
//        .addLast(ThrowableAdapter)
//        .addLast(ListAdapter)
//        .addLast(MapAdapter)
//        .asConfigurable()
//        .withStandardMappings()
//        .done()
//)
//
//fun main() {
//    val cat = KotshiCat(UUID.randomUUID(), "Kratos")
//
//    // serialize
//    val string = json.asFormatString(cat)
//        .also(::println)
//
//    // deserialize
//    json.asA<KotshiCat>(string)
//        .also(::println)
//
//    // make a lens
//    val lens = json.autoBody<KotshiCat>().toLens()
//    Request(Method.GET, "foo").with(lens of cat)
//}