Use Moshi with "lite" Reflection

This recipe will teach you how to replace Moshi's default kotlin-reflect backend with the lightweight kotlinx-metadata variant.

By default, the moshi-kotlin module uses kotlin-reflect, which is perfectly fine for most uses. However, at 3 MB, the relatively large JAR size of kotlin-reflect can have a meaningful performance impact in some scenarios: such as AWS Lambda cold-starts.

Moshi is likely to adopt kotlinx-metadata in the future, as you can see in this approved pull request. But until then, you can use this recipe to take advantage of the performance gains.

Gradle setup

This recipe uses the 3rd-party moshi-metadata-reflect module.

implementation("org.http4k:http4k-format-moshi:4.34.2.0") {
    exclude("com.squareup.moshi", "moshi-kotlin")
}
implementation("dev.zacsweers.moshix:moshi-metadata-reflect:0.19.0")

If you wish to take full advantage of the performance benefits, you need to ensure kotlin-reflect isn't bundled into your final jar. You must:

  1. Exclude the original moshi-kotlin module from http4k-format-moshi
  2. Ensure you don't have other 3rd-party libraries that depend on kotlin-reflect

Recipe

The first step is to define a "lite" version of ConfigurableMoshi. This is done by overriding the default kotlin-reflect backend with the kotlinx-metadata backend, via the 3rd-party moshi-metadata-reflect module.

package guide.howto.moshi_lite

import com.squareup.moshi.Moshi
import dev.zacsweers.moshix.reflect.MetadataKotlinJsonAdapterFactory
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

object MoshiLite: ConfigurableMoshi(
    Moshi.Builder()
        .addLast(EventAdapter)
        .addLast(ThrowableAdapter)
        .addLast(ListAdapter)
        .addLast(MapAdapter)
        .asConfigurable(MetadataKotlinJsonAdapterFactory()) // <-- moshi-metadata-reflect
        .withStandardMappings()
        .done()
)

Then you can use this new ConfigurableMoshi instance for auto-marshalling and lens creation, like normal.

package guide.howto.moshi_lite

import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.with
import java.util.UUID

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

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

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

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

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