Blog

Product news, updates, and insights from ej-technologies.

Read with RSS. Subscribe by email. Follow on .

Java vs. Kotlin: Who Can (De)serialize Faster?

2026-08-31
Posted by Ingo Kegel

In the previous post, I profiled Java serialization frameworks and found that Apache Fory beats Java serialization and other contenders by a huge margin. This generated some interest in the question, how the Kotlin frameworks compare. The Kotlin ecosystem has its own serialization libraries, most notably kotlinx.serialization, the official JetBrains library with compile-time code generation, and Moshi with its KSP-based adapter generation. Surely generated code beats the reflection-heavy Java options like Jackson?

To find out, I extended the comparison project with an equivalent Kotlin data model and profiled round trips with JProfiler. The answer is that the language of your model classes does not matter a lot. However, there are some really surprising finds.

The Numbers

The payload is the same as last time: a realistic object graph with shared references, collections, maps, enums, BigDecimal and byte arrays, two million serialize and deserialize round trips on JDK 25. The Kotlin libraries work on Kotlin data classes, the Jackson Kotlin row adds jackson-module-kotlin, and the Java rows use an equivalent Java bean model. First the JSON setups:

Setup Language Time per round trip Payload size Comment
Jackson JSON Java 25.5 µs 9,814 bytes fastest JSON engine
Jackson JSON Kotlin 29.5 µs 9,814 bytes 16% tax for Kotlin constructor binding
Moshi JSON Kotlin 31.8 µs 9,760 bytes KSP-generated adapters
Moshi JSON Java 37.0 µs 9,760 bytes reflective adapters
kotlinx JSON Kotlin 41.1 µs 9,769 bytes format-neutral protocol
Blog figureBlog figure

Second, the binary formats:

Setup Language Time per round trip Payload size Comment
Fory Java / Kotlin 3.5 µs / 3.6 µs 7,502 / 7,620 bytes test class names are a little longer for Kotlin
Jackson CBOR Java 12.0 µs 7,819 bytes
kotlinx Protobuf Kotlin 14.5 µs 7,155 bytes smallest payload, field numbers instead of names
kotlinx CBOR Kotlin 15.9 µs 7,797 bytes
java.io Java 46.8 µs 9,152 bytes default JDK binary format
Blog figureBlog figure

Jackson Is the JSON Champion

What really surprised me was that kotlinx.serialization, with its compile-time code generation, has the slowest JSON backend in the entire comparison. It loses out to Jackson's reflection-based data binding, even compared to the jackson-kotlin run.

The second unexpected outcome: If you need to serialize JSON in Kotlin (JVM), then Jackson is the top choice via its jackson-kotlin module. It is about 16% slower than the Java model because of KotlinValueInstantiator.createFromObjectWith (7.3%) and ValueCreator.getValueParameters (3.3%). That is the price of doing instantiation the Kotlin way, with parameter names, null-safety and default values.

Why is Jackson so fast? There are several reasons that sum up to give this result. One point that is related to the kind of payloads that we use in our test: JSON has no binary type, so the byte arrays in the test data must be base64-encoded. This is where the JSON engines differ most:

  • Jackson encodes base64 directly into its UTF-8 output buffer.
  • Moshi creates a base64 string, then scans it for escaping, then writes UTF-8.
  • kotlinx.serialization does the same multi-pass work and first builds the whole JSON document as a String, which is then converted to bytes.
Blog figureBlog figure

Code Generation Is Not Enough

kotlinx.serialization generates a serializer for each class at compile time, and yet it is slow. In the profiling data you can clearly see why that is the case: the generated serializers are for a format-neutral Encoder and Decoder protocol. Every object goes through beginStructure with a descriptor and every field goes through decodeElementIndex and per-element encode and decode calls. Also, every round trip allocates fresh encoder and decoder instances.

The cleanest measurement of this protocol cost is the Protobuf backend, because protobuf's wire format is minimal: just field numbers and native binary data. In that run, about 40% of the time spent in generated deserializers is from the decoder machinery: decodeElementIndex alone takes 24.7% and beginStructure 15.5%. The actual data movement then comes down to the Fory range.

Blog figureBlog figure

So kotlinx.serialization builds code at compile time and Fory at runtime, but that does not matter for performance. The important distinction is that kotlinx.serialization builds an abstract serializer and Fory builds a format-specific serializer. Generating the wire format directly makes all the difference.

This is not to say the code generation is not important for performance at all. If the only difference is code generation vs. reflection, then it does matter.

In the Kotlin model, Moshi uses KSP-generated adapters to write JSON directly. In the Java model, Moshi uses reflective field adapters, which are 16% slower than the generated adapters in the Kotlin model.

Advantages of kotlinx.serialization

While these numbers don't really favor kotlinx.serialization, it has a lot going for it, and I'm a heavy user of it myself for those reasons.

kotlinx.serialization includes CBOR and Protobuf backends with much better performance: Protobuf at 14.5 µs has the smallest payload of the entire comparison (7,155 bytes) and CBOR comes in at 15.9 µs. The CBOR decoder does a HashMap lookup per field name (11% of the run), because CBOR is self-describing and writes field names, while protobuf just uses field numbers.

First, the format-neutral protocol is not a bug, it is a feature. When you annotate a class with @Serializable, it works with JSON, CBOR, Protobuf and other third-party formats. Second, it works on the JVM, on Android, on iOS, in the browser and even in native code. Fory's runtime code generation is JVM-specific machinery.

If you need Kotlin Multiplatform and want to switch formats without changing your model, kotlinx.serialization is the only game in town, and its binary backends are respectably fast. Just do not pick its JSON backend for raw speed.

Choosing a Library for Kotlin

  • For pure speed on the JVM, Fory works on Kotlin data classes exactly as well as on Java classes. And no annotations are needed at all.
  • For JSON on the JVM, Jackson with jackson-module-kotlin is the fastest option, with a 16% Kotlin tax for null-safety and default values. Moshi with KSP codegen is a close and respectable second.
  • For Kotlin Multiplatform, kotlinx.serialization is the default choice. Faster CBOR and Protobuf backends are available when applicable.

How This Was Measured

The measuring setup was the same as in the previous post: the JProfiler Gradle plugin recorded one snapshot per setup in offline mode, with CPU sampling or allocation recording in separate runs. A coding agent drove the JProfiler MCP server for the hot spot analysis. The full project is on GitHub.

To look at the snapshots yourself, download JProfiler and open the .jps files from build/snapshots-cpu.

Archive