Java vs. Kotlin: Who Can (De)serialize Faster?
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 |


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 |


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.


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.


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-kotlinis 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.