kotlinx.serialization

Kotlin multiplatform / multi-format serialization

RAW Doc

1. Project Overview & Quickstart (Kotlin/kotlinx.serialization)

File: README.md

Kotlin multiplatform / multi-format reflectionless serialization








Kotlin serialization consists of a compiler plugin, that generates visitor code for serializable classes,
runtime library with core serialization API and support libraries with various serialization formats.

  • Supports Kotlin classes marked as @Serializable and standard collections.
  • Provides JSON, Protobuf, CBOR, Hocon and Properties formats.
  • Complete multiplatform support: JVM, JS and Native.

Table of contents

Introduction and references

Here is a small example.

kotlin
import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable 
data class Project(val name: String, val language: String)

fun main() {
    // Serializing objects
    val data = Project("kotlinx.serialization", "Kotlin")
    val string = Json.encodeToString(data)  
    println(string) // {"name":"kotlinx.serialization","language":"Kotlin"} 
    // Deserializing back into objects
    val obj = Json.decodeFromString(string)
    println(obj) // Project(name=kotlinx.serialization, language=Kotlin)
}

You can get the full code here.

Read the Kotlin Serialization Guide for all details.

You can find auto-generated documentation website on kotlinlang.org.

Setup

New versions of the serialization plugin are released in tandem with each new Kotlin compiler version.

Make sure you have the corresponding Kotlin plugin installed in the IDE, no additional plugins for IDE are required.

Gradle

To set up kotlinx.serialization, you have to do two things:

  1. Add the serialization plugin.
  2. Add the serialization library dependency.

1) Setting up the serialization plugin

You can set up the serialization plugin with the Kotlin plugin using the
Gradle plugins DSL:

Kotlin DSL:

kotlin
plugins {
    kotlin("jvm") version "2.3.20" // or kotlin("multiplatform") or any other kotlin plugin
    kotlin("plugin.serialization") version "2.3.20"
}

Groovy DSL:

gradle
plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '2.3.20'
    id 'org.jetbrains.kotlin.plugin.serialization' version '2.3.20'
}

Kotlin versions before 1.4.0 are not supported by the stable release of Kotlin serialization.

Using apply plugin (the old way)

First, you have to add the serialization plugin to your classpath as the other compiler plugins:

Kotlin DSL:

kotlin
buildscript {
    repositories { mavenCentral() }

    dependencies {
        val kotlinVersion = "2.3.20"
        classpath(kotlin("gradle-plugin", version = kotlinVersion))
        classpath(kotlin("serialization", version = kotlinVersion))
    }
}

Groovy DSL:

gradle
buildscript {
    ext.kotlin_version = '2.3.20'
    repositories { mavenCentral() }

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
    }
}

Then you can apply plugin (example in Groovy):

gradle
apply plugin: 'kotlin' // or 'kotlin-multiplatform' for multiplatform projects
apply plugin: 'kotlinx-serialization'

2) Dependency on the JSON library

After setting up the plugin, you have to add a dependency on the serialization library.
Note that while the plugin has version the same as the compiler one, runtime library has different coordinates, repository and versioning.

Kotlin DSL:

kotlin
repositories {
    mavenCentral()
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
}

Groovy DSL:

gradle
repositories {
    mavenCentral()
}

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0"
}

We also provide kotlinx-serialization-core artifact that contains all serialization API but does not have a bundled serialization format with it

Android

By default, proguard rules are supplied with the library.
These rules keep serializers for all serializable classes that are retained after shrinking,
so you don't need additional setup.

However, these rules do not affect serializable classes if they have named companion objects.

If you want to serialize classes with named companion objects, you need to add and edit rules below to your proguard-rules.pro configuration.

Note that the rules for R8 differ depending on the compatibility mode used.

Example of named companion rules for ProGuard and R8 compatibility mode

proguard
# Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`.
# If you have any, replace classes with those containing named companion objects.
-keepattributes InnerClasses # Needed for `getDeclaredClasses`.

-if @kotlinx.serialization.Serializable class
com.example.myapplication.HasNamedCompanion, # <-- List serializable classes with named companions.
com.example.myapplication.HasNamedCompanion2
{
    static **$* *;
}
-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept.
    static <1>$$serializer INSTANCE;
}

Example of named companion rules for R8 full mode

proguard
# Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`.
# If you have any, replace classes with those containing named companion objects.
-keepattributes InnerClasses # Needed for `getDeclaredClasses`.

-if @kotlinx.serialization.Serializable class
com.example.myapplication.HasNamedCompanion, # <-- List serializable classes with named companions.
com.example.myapplication.HasNamedCompanion2
{
    static **$* *;
}
-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept.
    static <1>$$serializer INSTANCE;
}

# Keep both serializer and serializable classes to save the attribute InnerClasses
-keepclasseswithmembers, allowshrinking, allowobfuscation, allowaccessmodification class
com.example.myapplication.HasNamedCompanion, # <-- List serializable classes with named companions.
com.example.myapplication.HasNamedCompanion2
{
    *;
}

In case you want to exclude serializable classes that are used, but never serialized at runtime,
you will need to write custom rules with narrower class specifications.

Multiplatform (Common, JS, Native)

Most of the modules are also available for Kotlin/JS and Kotlin/Native.
You can add dependency to the required module right to the common source set:

gradle
commonMain {
    dependencies {
        // Works as common dependency as well as the platform one
        implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version"
    }
}

The same artifact coordinates can be used to depend on platform-specific artifact in platform-specific source-set.

Maven

Ensure the proper version of Kotlin and serialization version:

xml
<kotlin.version>2.3.20</kotlin.version>
    <serialization.version>1.11.0</serialization.version>

Add serialization plugin to Kotlin compiler plugin:

xml
<groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-plugin</artifactId>
            <version>${kotlin.version}</version>
            <executions>
                <execution>
                    compile
                    compile
                    <goals>
                        <goal>compile</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <compilerPlugins>
                    kotlinx-serialization
                </compilerPlugins>
            </configuration>
            <dependencies>
                <dependency>
                    <groupId>org.jetbrains.kotlin</groupId>
                    <artifactId>kotlin-maven-serialization</artifactId>
                    <version>${kotlin.version}</version>
                </dependency>
            </dependencies>

Add dependency on serialization runtime library:

xml
<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-serialization-json</artifactId>
    <version>${serialization.version}</version>
</dependency>

Bazel

To setup the Kotlin compiler plugin for Bazel, follow the
example

from the rules_kotlin repository.


File: docs/basic-serialization.md

Basic Serialization

This is the first chapter of the Kotlin Serialization Guide.
This chapter shows the basic use of Kotlin Serialization and explains its core concepts.

Table of contents

Basics

To convert an object tree to a string or to a sequence of bytes, it must come
through two mutually intertwined processes. In the first step, an object is serialized—it
is converted into a serial sequence of its constituting primitive values. This process is common for all
data formats and its result depends on the object being serialized. A serializer controls this process.
The second step is called encoding—it is the conversion of the corresponding sequence of primitives into
the output format representation. An encoder controls this process. Whenever the distinction is not important,
both the terms of encoding and serialization are used interchangeably.

text
+---------+  Serialization  +------------+  Encoding  +---------------+
| Objects | --------------> | Primitives | ---------> | Output format |
+---------+                 +------------+            +---------------+

The reverse process starts with parsing of the input format and decoding of primitive values,
followed by deserialization of the resulting stream into objects. We'll see details of this process later.

For now, we start with JSON encoding.

JSON encoding

The whole process of converting data into a specific format is called encoding. For JSON we encode data
using the Json.encodeToString extension function. It serializes
the object that is passed as its parameter under the hood and encodes it to a JSON string.

Let's start with a class describing a project and try to get its JSON representation.

kotlin
class Project(val name: String, val language: String)

fun main() {
    val data = Project("kotlinx.serialization", "Kotlin")
    println(Json.encodeToString(data))
}

You can get the full code here.

When we run this code we get the exception.

text
Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Project' is not found.
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.

Serializable classes have to be explicitly marked. Kotlin Serialization does not use reflection,
so you cannot accidentally deserialize a class which was not supposed to be serializable. We fix it by
adding the @Serializable annotation.

kotlin
@Serializable
class Project(val name: String, val language: String)

fun main() {
    val data = Project("kotlinx.serialization", "Kotlin")
    println(Json.encodeToString(data))
}

You can get the full code here.

The @Serializable annotation instructs the Kotlin Serialization plugin to automatically generate and hook
up a serializer for this class. Now the output of the example is the corresponding JSON.

text
{"name":"kotlinx.serialization","language":"Kotlin"}

There is a whole chapter about the Serializers. For now, it is enough to know
that they are automatically generated by the Kotlin Serialization plugin.

JSON decoding

The reverse process is called decoding. To decode a JSON string into an object, we'll
use the Json.decodeFromString extension function.
To specify which type we want to get as a result, we provide a type parameter to this function.

As we'll see later, serialization works with different kinds of classes.
Here we are marking our Project class as a data class, not because it is required, but because
we want to print its contents to verify how it decodes.

kotlin
@Serializable
data class Project(val name: String, val language: String)

fun main() {
    val data = Json.decodeFromString("""
        {"name":"kotlinx.serialization","language":"Kotlin"}
    """)
    println(data)
}

You can get the full code here.

Running this code we get back the object.

text
Project(name=kotlinx.serialization, language=Kotlin)

Serializable classes

This section goes into more details on how different @Serializable classes are handled.

Backing fields are serialized

Only a class's properties with backing fields are serialized, so properties with a getter/setter that don't
have a backing field and delegated properties are not serialized, as the following example shows.

kotlin
@Serializable
class Project(
    // name is a property with backing field -- serialized
    var name: String
) {
    var stars: Int = 0 // property with a backing field -- serialized

    val path: String // getter only, no backing field -- not serialized
        get() = "kotlin/$name"

    var id by ::name // delegated property -- not serialized
}

fun main() {
    val data = Project("kotlinx.serialization").apply { stars = 9000 }
    println(Json.encodeToString(data))
}

You can get the full code here.

We can clearly see that only the name and stars properties are present in the JSON output.

text
{"name":"kotlinx.serialization","stars":9000}

Constructor properties requirement

If we want to define the Project class so that it takes a path string, and then
deconstructs it into the corresponding properties, we might be tempted to write something like the code below.

kotlin
@Serializable
class Project(path: String) {
    val owner: String = path.substringBefore('/')
    val name: String = path.substringAfter('/')
}

This class does not compile because the @Serializable annotation requires that all parameters of the class's primary
constructor be properties. A simple workaround is to define a private primary constructor with the class's
properties, and turn the constructor we wanted into the secondary one.

kotlin
@Serializable
class Project private constructor(val owner: String, val name: String) {
    constructor(path: String) : this(
        owner = path.substringBefore('/'),
        name = path.substringAfter('/')
    )

    val path: String
        get() = "$owner/$name"
}

Serialization works with a private primary constructor, and still serializes only backing fields.

kotlin
fun main() {
    println(Json.encodeToString(Project("kotlin/kotlinx.serialization")))
}

You can get the full code here.

This example produces the expected output.

text
{"owner":"kotlin","name":"kotlinx.serialization"}

Data validation

Another case where you might want to introduce a primary constructor parameter without a property is when you
want to validate its value before storing it to a property. To make it serializable you shall replace it
with a property in the primary constructor, and move the validation to an init { ... } block.

kotlin
@Serializable
class Project(val name: String) {
    init {
        require(name.isNotEmpty()) { "name cannot be empty" }
    }
}

A deserialization process works like a regular constructor in Kotlin and calls all init blocks, ensuring that you
cannot get an invalid class as a result of deserialization. Let's try it.

kotlin
fun main() {
    val data = Json.decodeFromString("""
        {"name":""}
    """)
    println(data)
}

You can get the full code here.

Running this code produces the exception:

text
Exception in thread "main" java.lang.IllegalArgumentException: name cannot be empty

Optional properties

An object can be deserialized only when all its properties are present in the input.
For example, run the following code.

kotlin
@Serializable
data class Project(val name: String, val language: String)

fun main() {
    val data = Json.decodeFromString("""
        {"name":"kotlinx.serialization"}
    """)
    println(data)
}

You can get the full code here.

It produces the exception:

text
Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'example.exampleClasses04.Project', but it was missing at path: $

This problem can be fixed by adding a default value to the property, which automatically makes it optional
for serialization.

kotlin
@Serializable
data class Project(val name: String, val language: String = "Kotlin")

fun main() {
    val data = Json.decodeFromString("""
        {"name":"kotlinx.serialization"}
    """)
    println(data)
}

You can get the full code here.

It produces the following output with the default value for the language property.

text
Project(name=kotlinx.serialization, language=Kotlin)

Optional property initializer call

When an optional property is present in the input, the corresponding initializer for this
property is not even called. This is a feature designed for performance, so be careful not
to rely on side effects in initializers. Consider the example below.

kotlin
fun computeLanguage(): String {
    println("Computing")
    return "Kotlin"
}

@Serializable
data class Project(val name: String, val language: String = computeLanguage())

fun main() {
    val data = Json.decodeFromString("""
        {"name":"kotlinx.serialization","language":"Kotlin"}
    """)
    println(data)
}

You can get the full code here.

Since the language property was specified in the input, we don't see the "Computing" string printed
in the output.

text
Project(name=kotlinx.serialization, language=Kotlin)

Required properties

A property with a default value can be required in a serial format with the @Required annotation.
Let us change the previous example by marking the language property as @Required.

kotlin
@Serializable
data class Project(val name: String, @Required val language: String = "Kotlin")

fun main() {
    val data = Json.decodeFromString("""
        {"name":"kotlinx.serialization"}
    """)
    println(data)
}

You can get the full code here.

We get the following exception.

text
Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'example.exampleClasses07.Project', but it was missing at path: $

Transient properties

A property can be excluded from serialization by marking it with the @Transient annotation
(don't confuse it with kotlin.jvm.Transient). Transient properties must have a default value.

kotlin
@Serializable
data class Project(val name: String, @Transient val language: String = "Kotlin")

fun main() {
    val data = Json.decodeFromString("""
        {"name":"kotlinx.serialization","language":"Kotlin"}
    """)
    println(data)
}

You can get the full code here.

Attempts to explicitly specify its value in the serial format, even if the specified
value is equal to the default one, produces the following exception.

text
Exception in thread "main" kotlinx.serialization.json.JsonDecodingException: Unexpected JSON token at offset 42: Encountered an unknown key 'language' at path: $
Use 'ignoreUnknownKeys = true' in 'Json {}' builder or '@JsonIgnoreUnknownKeys' annotation to ignore unknown keys.

The 'ignoreUnknownKeys' feature is explained in the Ignoring Unknown Keys section section.

Defaults are not encoded by default

Default values are not encoded by default in JSON. This behavior is motivated by the fact that in most real-life scenarios
such configuration reduces visual clutter, and saves the amount of data being serialized.

kotlin
@Serializable
data class Project(val name: String, val language: String = "Kotlin")

fun main() {
    val data = Project("kotlinx.serialization")
    println(Json.encodeToString(data))
}

You can get the full code here.

It produces the following output, which does not have the language property because its value is equal to the default one.

text
{"name":"kotlinx.serialization"}

See JSON's Encoding defaults section on how this behavior can be configured for JSON.
Additionally, this behavior can be controlled without taking format settings into account.
For that purposes, EncodeDefault annotation can be used:

kotlin
@Serializable
data class Project(
    val name: String,
    @EncodeDefault val language: String = "Kotlin"
)

This annotation instructs the framework to always serialize property, regardless of its value or format settings.
It's also possible to tweak it into the opposite behavior using EncodeDefault.Mode parameter:

kotlin
@Serializable
data class User(
    val name: String,
    @EncodeDefault(NEVER) val projects: List = emptyList()
)

fun main() {
    val userA = User("Alice", listOf(Project("kotlinx.serialization")))
    val userB = User("Bob")
    println(Json.encodeToString(userA))
    println(Json.encodeToString(userB))
}

You can get the full code here.

As you can see, language property is preserved and projects is omitted:

text
{"name":"Alice","projects":[{"name":"kotlinx.serialization","language":"Kotlin"}]}
{"name":"Bob"}

Nullable properties

Nullable properties are natively supported by Kotlin Serialization.

kotlin
@Serializable
class Project(val name: String, val renamedTo: String? = null)

fun main() {
    val data = Project("kotlinx.serialization")
    println(Json.encodeToString(data))
}

You can get the full code here.

This example does not encode null in JSON because Defaults are not encoded.

text
{"name":"kotlinx.serialization"}

Type safety is enforced

Kotlin Serialization strongly enforces the type safety of the Kotlin programming language.
In particular, let us try to decode a null value from a JSON object into a non-nullable Kotlin property language.

kotlin
@Serializable
data class Project(val name: String, val language: String = "Kotlin")

fun main() {
    val data = Json.decodeFromString("""
        {"name":"kotlinx.serialization","language":null}
    """)
    println(data)
}

You can get the full code here.

Even though the language property has a default value, it is still an error to attempt to assign
the null value to it.

text
Exception in thread "main" kotlinx.serialization.json.JsonDecodingException: Unexpected JSON token at offset 52: Expected string literal but 'null' literal was found at path: $.language
Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value.

It might be desired, when decoding 3rd-party JSONs, to coerce null to a default value.
The corresponding feature is explained in the Coercing input values section.

Referenced objects

Serializable classes can reference other classes in their serializable properties.
The referenced classes must be also marked as @Serializable.

kotlin
@Serializable
class Project(val name: String, val owner: User)

@Serializable
class User(val name: String)

fun main() {
    val owner = User("kotlin")
    val data = Project("kotlinx.serialization", owner)
    println(Json.encodeToString(data))
}

You can get the full code here.

When encoded to JSON it results in a nested JSON object.

text
{"name":"kotlinx.serialization","owner":{"name":"kotlin"}}

References to non-serializable classes can be marked as Transient properties, or a
custom serializer can be provided for them as shown in the Serializers chapter.

No compression of repeated references

Kotlin Serialization is designed for encoding and decoding of plain data. It does not support reconstruction
of arbitrary object graphs with repeated object references. For example, let us try to serialize an object
that references the same owner instance twice.

kotlin
@Serializable
class Project(val name: String, val owner: User, val maintainer: User)

@Serializable
class User(val name: String)

fun main() {
    val owner = User("kotlin")
    val data = Project("kotlinx.serialization", owner, owner)
    println(Json.encodeToString(data))
}

You can get the full code here.

We simply get the owner value encoded twice.

text
{"name":"kotlinx.serialization","owner":{"name":"kotlin"},"maintainer":{"name":"kotlin"}}

Attempt to serialize a circular structure will result in stack overflow.
You can use the Transient properties to exclude some references from serialization.

Generic classes

Generic classes in Kotlin provide type-polymorphic behavior, which is enforced by Kotlin Serialization at
compile-time. For example, consider a generic serializable class Box<T>.

kotlin
@Serializable
class Box<T>(val contents: T)

The Box<T> class can be used with builtin types like Int, as well as with user-defined types like Project.

kotlin
@Serializable
class Data(
    val a: Box,
    val b: Box
)

fun main() {
    val data = Data(Box(42), Box(Project("kotlinx.serialization", "Kotlin")))
    println(Json.encodeToString(data))
}

You can get the full code here.

The actual type that we get in JSON depends on the actual compile-time type parameter that was specified for Box.

text
{"a":{"contents":42},"b":{"contents":{"name":"kotlinx.serialization","language":"Kotlin"}}}

If the actual generic type is not serializable a compile-time error will be produced.

Serial field names

The names of the properties used in encoded representation, JSON in our examples, are the same as
their names in the source code by default. The name that is used for serialization is called a serial name, and
can be changed using the @SerialName annotation. For example, we can have a language property in
the source with an abbreviated serial name.

kotlin
@Serializable
class Project(val name: String, @SerialName("lang") val language: String)

fun main() {
    val data = Project("kotlinx.serialization", "Kotlin")
    println(Json.encodeToString(data))
}

You can get the full code here.

Now we see that an abbreviated name lang is used in the JSON output.

text
{"name":"kotlinx.serialization","lang":"Kotlin"}

The next chapter covers Builtin classes.


File: docs/building.md

Building Kotlin Serialization from the source

JDK version

To build Kotlin Serialization JDK version 11 or higher is required. Make sure this is your default JDK (JAVA_HOME is set accordingly).
This is needed to compile the module-info file included for JPMS support.

In case you are determined to use a different JDK version or experience problems with JPMS, you can turn off compilation of module-info files
completely with disableJPMS property: add disableJPMS=true to gradle.properties or -PdisableJPMS to Gradle CLI invocation.

Runtime library

Kotlin Serialization runtime library itself is a multiplatform project.
To build the library from the source and run all tests, use ./gradlew build. Corresponding platform tasks like jvmTest, jsTest, macosArm64Test, and so on are also available.

The project can be opened in IntelliJ IDEA without additional prerequisites.
In case you want to work with Protobuf tests, you may need to run ./gradlew generateTestProto beforehand.

To install runtime library into the local Maven repository, run ./gradlew publishToMavenLocal.
After that, you can include this library in arbitrary projects like usual gradle dependency:

gradle
repositories {
    mavenLocal()
}

dependencies {
    compile "org.jetbrains.kotlinx:kotlinx-serialization-core:$serialization_version"
}

To use snapshot version of compiler (if you have built and installed it from sources), use flag -Pbootstrap.
If you have built both Kotlin and Kotlin/Native compilers, set kotlin.native.home property in gradle.properties to the path with Kotlin/Native distribution
(usually kotlin-native/dist folder inside Kotlin project).

The master and dev branches of the library should be binary compatible with the latest released compiler plugin. In case you want to test some new features from other branches,
which are still in development and may not be compatible in terms of bytecode produced by plugin, you'll need to build the plugin by yourself.

Compiler plugin

Compiler plugins for Gradle/Maven and IntelliJ plugin, starting from Kotlin 1.3, are embedded into the Kotlin compiler.

Sources and steps to build it are located here.
In short, you'll just need to run ./gradlew dist install to get 2.x.255-SNAPSHOT versions of Kotlin compiler, stdlib, and serialization plugins in the Maven local repository.


File: docs/builtin-classes.md

Builtin classes

This is the second chapter of the Kotlin Serialization Guide.
In addition to all the primitive types and strings, serialization for some classes from the Kotlin standard library,
including the standard collections, is built into Kotlin Serialization. This chapter explains the details.

Table of contents

Primitives

Kotlin Serialization has the following ten primitives:
Boolean, Byte, Short, Int, Long, Float, Double, Char, String, and enums.
The other types in Kotlin Serialization are composite—composed of those primitive values.

Numbers

All types of integer and floating-point Kotlin numbers can be serialized.

kotlin
@Serializable
class Data(
    val answer: Int,
    val pi: Double
)                     

fun main() {
    val data = Data(42, PI)
    println(Json.encodeToString(data))
}

You can get the full code here.

Their natural representation in JSON is used.

text
{"answer":42,"pi":3.141592653589793}

Long numbers

Long integers are serializable, too.

kotlin
@Serializable
class Data(val signature: Long)

fun main() {
    val data = Data(0x1CAFE2FEED0BABE0)
    println(Json.encodeToString(data))
}

You can get the full code here.

By default they are serialized to JSON as numbers.

text
{"signature":2067120338512882656}

Long numbers as strings

The JSON output from the previous example will get decoded normally by Kotlin Serialization running on Kotlin/JS.
However, if we try to parse this JSON by native JavaScript methods, we get this truncated result.

text
JSON.parse("{\"signature\":2067120338512882656}")
▶ {signature: 2067120338512882700}

The full range of a Kotlin Long does not fit in the JavaScript number, so its precision gets lost in JavaScript.
A common workaround is to represent long numbers with full precision using the JSON string type.
This approach is optionally supported by Kotlin Serialization with LongAsStringSerializer, which
can be specified for a given Long property using the @Serializable annotation:

kotlin
@Serializable
class Data(
    @Serializable(with=LongAsStringSerializer::class)
    val signature: Long
)

fun main() {
    val data = Data(0x1CAFE2FEED0BABE0)
    println(Json.encodeToString(data))
}

You can get the full code here.

This JSON gets parsed natively by JavaScript without loss of precision.

text
{"signature":"2067120338512882656"}

The section on Specifying serializers for a file explains how a
serializer like LongAsStringSerializer can be specified for all properties in a file.

Enum classes

All enum classes are serializable out of the box without having to mark them @Serializable,
as the following example shows.

kotlin
// The @Serializable annotation is not needed for enum classes
enum class Status { SUPPORTED }
        
@Serializable
class Project(val name: String, val status: Status) 

fun main() {
    val data = Project("kotlinx.serialization", Status.SUPPORTED)
    println(Json.encodeToString(data))
}

You can get the full code here.

In JSON an enum gets encoded as a string.

text
{"name":"kotlinx.serialization","status":"SUPPORTED"}

Note: On Kotlin/JS and Kotlin/Native, @Serializable annotation is needed for enum class if you want to use it as a root object — i.e. use encodeToString<Status>(Status.SUPPORTED).

Serial names of enum entries

Serial names of enum entries can be customized with the SerialName annotation just like
it was shown for properties in the Serial field names section.
However, in this case, the whole enum class must be marked with the @Serializable annotation.

kotlin
@Serializable // required because of @SerialName
enum class Status { @SerialName("maintained") SUPPORTED }
        
@Serializable
class Project(val name: String, val status: Status) 

fun main() {
    val data = Project("kotlinx.serialization", Status.SUPPORTED)
    println(Json.encodeToString(data))
}

You can get the full code here.

We see that the specified serial name is now used in the resulting JSON.

text
{"name":"kotlinx.serialization","status":"maintained"}

Composites

A number of composite types from the standard library are supported by Kotlin Serialization.

Pair and triple

The simple data classes Pair and Triple from the Kotlin standard library are serializable.

kotlin
@Serializable
class Project(val name: String)

fun main() {
    val pair = 1 to Project("kotlinx.serialization")
    println(Json.encodeToString(pair))
}

You can get the full code here.

text
{"first":1,"second":{"name":"kotlinx.serialization"}}

Not all classes from the Kotlin standard library are serializable. In particular, ranges and the Regex class
are not serializable at the moment. Support for their serialization may be added in the future.

Lists

A List of serializable classes can be serialized.

kotlin
@Serializable
class Project(val name: String)

fun main() {
    val list = listOf(
        Project("kotlinx.serialization"),
        Project("kotlinx.coroutines")    
    )
    println(Json.encodeToString(list))
}

You can get the full code here.

The result is represented as a list in JSON.

text
[{"name":"kotlinx.serialization"},{"name":"kotlinx.coroutines"}]

Sets and other collections

Other collections, like Set, are also serializable.

kotlin
@Serializable
class Project(val name: String)

fun main() {
    val set = setOf(
        Project("kotlinx.serialization"),
        Project("kotlinx.coroutines")    
    )
    println(Json.encodeToString(set))
}

You can get the full code here.

Set is also represented as a list in JSON, like all other collections.

text
[{"name":"kotlinx.serialization"},{"name":"kotlinx.coroutines"}]

Deserializing collections

During deserialization, the type of the resulting object is determined by the static type that was specified
in the source code—either as the type of the property or as the type parameter of the decoding function.
The following example shows how the same JSON list of integers is deserialized into two properties of
different Kotlin types.

kotlin
@Serializable
data class Data(
    val a: List,
    val b: Set
)
     
fun main() {
    val data = Json.decodeFromString<Data>("""
        {
            "a": [42, 42],
            "b": [42, 42]
        }
    """)
    println(data)
}

You can get the full code here.

Because the data.b property is a Set, the duplicate values from it disappeared.

text
Data(a=[42, 42], b=[42])

Maps

A Map with primitive or enum keys and arbitrary serializable values can be serialized.

kotlin
@Serializable
class Project(val name: String)

fun main() {
    val map = mapOf(
        1 to Project("kotlinx.serialization"),
        2 to Project("kotlinx.coroutines")    
    )
    println(Json.encodeToString(map))
}

You can get the full code here.

Kotlin maps in JSON are represented as objects. In JSON object keys are always strings, so keys are encoded as strings
even if they are numbers in Kotlin, as we can see below.

text
{"1":{"name":"kotlinx.serialization"},"2":{"name":"kotlinx.coroutines"}}

It is a JSON-specific limitation that keys cannot be composite.
It can be lifted as shown in the Allowing structured map keys section.

Unit and singleton objects

The Kotlin builtin Unit type is also serializable.
Unit is a Kotlin singleton object,
and is handled equally with other Kotlin objects.

Conceptually, a singleton is a class with only one instance, meaning that state does not define the object,
but the object defines its state. In JSON, objects are serialized as empty structures.

kotlin
@Serializable
object SerializationVersion {
    val libraryVersion: String = "1.0.0"
}

fun main() {
    println(Json.encodeToString(SerializationVersion))
    println(Json.encodeToString(Unit))
}

You can get the full code here.

While it may seem useless at first glance, this comes in handy for sealed class serialization,
which is explained in the Polymorphism. Objects section.

text
{}
{}

Serialization of objects is format specific. Other formats may represent objects differently,
e.g. using their fully qualified names.

Duration

Since Kotlin 1.7.20 the Duration class has become serializable.

kotlin
fun main() {
    val duration = 1000.toDuration(DurationUnit.SECONDS)
    println(Json.encodeToString(duration))
}

You can get the full code here.

Duration is serialized as a string in the ISO-8601-2 format.

text
"PT16M40S"

Nothing

By default, Nothing is a serializable class. However, since there are no instances of this class, it is impossible to encode or decode its values - any attempt will cause an exception.

This serializer is used when syntactically some type is needed, but it is not actually used in serialization. For example, when using parameterized polymorphic base classes:

kotlin
@Serializable
sealed class ParametrizedParent<out R> {
    @Serializable
    data class ChildWithoutParameter(val value: Int) : ParametrizedParent<Nothing>()
}

fun main() {
    println(Json.encodeToString(ParametrizedParent.ChildWithoutParameter(42)))
}

You can get the full code here.

When encoding, the serializer for Nothing was not used

text
{"value":42}

The next chapter covers Serializers.


File: docs/compatibility.md

Compatibility policy

This document describes the compatibility policy of kotlinx.serialization library since version 1.0.0 and Kotlin 1.4.0.

Note that content of this document is applicable only for stable Kotlin platforms (currently Kotlin/JVM and classic Kotlin/JS),
since other experimental platforms currently do not impose any backward-compatibility guarantees.
You can check out what platforms are considered to be stable on this page.

Core library compatibility

Core library public API comes in three flavours: general (stable), experimental, and internal.
All public API except stable is marked with the corresponding annotation.
To learn how to use declarations that require opt-in, please refer to corresponding documentation page.

Stable API

Stable API is guaranteed to preserve its ABI and documented semantics:

  • It cannot change its semantics expressed in its documentation.
  • It is binary backwards-compatible: during update of kotlinx.serialization version, previously compiled code will continue to work.
    For example, for a library that depends only on kotlinx.serialization stable API,
    clients of the library can easily depend on a next kotlinx.serialization version and expect everything to work.
  • It is source backwards compatible modulo major deprecation. Most of the API is here to stay forever,
    unless an unfixable security or design flaw is exposed. Minor releases never add source-incompatible changes to the stable API.

Deprecation cycle

When API is deprecated, it goes through multiple stages and there is at least one major release between each stages.

  1. Feature is deprecated with compilation warning. Most of the time, proper replacement (and corresponding replaceWith declaration) is provided to automatically migrate deprecated usages with a help of IntelliJ IDEA.
  2. Deprecation level is increased to error or hidden. It is no longer possible to compile new code against deprecated API, though it is still present in the ABI.
  3. API is completely removed. While we give our best efforts not to do so and have no plans of removing any API, we still are leaving this option in case of unforeseen problems such as security issues.

Experimental API

This API marked as @ExperimentalSerializationApi. API is marked experimental when its design has potential open questions which may eventually lead to either semantics changes of the API or its deprecation.
By default, most of the new API is marked as experimental and becomes stable in one of the next releases if no new issues arise. Otherwise, either semantics is fixed without changes in ABI or API goes through deprecation cycle.

However, we'll try to provide best-effort compatibility — such declarations won't be deleted or changed instantly,
they will go through deprecation cycle if this is possible. However, this deprecation cycle may be faster than usual.

Usage notes:

  • Experimental API can be used in your applications if maintenance cost is clear:
    additional migrations may have to be performed during kotlinx.serialization update.

  • Experimental API can be used in other experimental API (for example, a custom serialization format).
    In such cases, clients of the API have to be aware about experimentality.

  • It's not recommended to use it as a dependency in your stable API, even as an implementation detail.
    Due to the lack of binary backward compatibility, your clients may experience behavioural changes
    or runtime exceptions when an unexpected version of kotlinx.serialization gets included in the runtime classpath.

Internal API

This API is marked with @InternalSerializationApi or located in kotlinx.serialization.internal package.
It does not have any binary or source compatibility guarantees and can be deprecated or deleted without replacement at any time.

It is not recommended to use it.
However, if you have a rare use-case that can be solved only with internal API, it is possible to use it.
In such a case, please create an issue on GitHub in order for us to understand a use-case and to provide stable alternative.

Compatibility with Kotlin compiler plugin

kotlinx.serialization also has the compiler plugin, that generates code depending on the core library.
Therefore, the compiler plugin should be compatible with the runtime library to work.
Kotlin & kotlinx.serialization plugin 1.4.0/1.4.10 are compatible with 1.0.0 runtime library.

For further updates, we have the following policy:

  • New Kotlin compiler plugins should be backward compatible with core library.
    It means that it is possible to freely update Kotlin version in a project without changing the code
    and without the need to update kotlinx.serialization runtime.
    In other words, 1.0.0 runtime can be used with any of Kotlin 1.4.x versions.

  • New Kotlin compiler plugin features may require new kotlinx.serialization library.
    For example, if Kotlin 1.4.x gets serialization of unsigned integers,
    it would require a corresponding runtime version higher than 1.0.0.
    This would be indicated by a compiler error specific to a particular feature.

  • New core library versions may or may not require Kotlin compiler plugin update,
    depending on a particular release.
    We'll try to avoid these situations; however, in case of some unexpected issues, it may be necessary.
    So it is possible to have a situation where upgrading serialization runtime from 1.a.0 to 1.b.0 requires an update of Kotlin version from 1.x.0 to 1.y.0 (y > x).
    The compiler can detect such problems and will inform you if its version is incompatible with a current version of core library.

Note that according to general rules for Kotlin binaries,
library is mostly forwards compatible with the next language release, but not later ones.
It means that if kotlinx.serialization 1.x was compiled with Kotlin 2.Y, it is compatible with Kotlin 2.(Y + 1), but not
2.(Y + 2).


File: docs/formats.md

Alternative and custom formats (experimental)

This is the sixth chapter of the Kotlin Serialization Guide.
It goes beyond JSON, covering alternative and custom formats. Unlike JSON, which is
stable, these are currently experimental features of Kotlin Serialization.

Table of contents

CBOR (experimental)

CBOR is one of the standard compact binary
encodings for JSON, so it supports a subset of JSON features and
is generally very similar to JSON in use, but produces binary data.

CBOR support is (experimentally) available in a separate
org.jetbrains.kotlinx:kotlinx-serialization-cbor:<version> module.

Cbor class has Cbor.encodeToByteArray and Cbor.decodeFromByteArray functions.
Let us take the basic example from the JSON encoding,
but encode it using CBOR.

kotlin
@Serializable
data class Project(val name: String, val language: String)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val data = Project("kotlinx.serialization", "Kotlin") 
    val bytes = Cbor.encodeToByteArray(data)   
    println(bytes.toAsciiHexString())
    val obj = Cbor.decodeFromByteArray(bytes)
    println(obj)
}

You can get the full code here.

We print a filtered ASCII representation of the output, writing non-ASCII data in hex, so we see how
all the original strings are directly represented in CBOR, but the format delimiters themselves are binary.

text
{BF}dnameukotlinx.serializationhlanguagefKotlin{FF}
Project(name=kotlinx.serialization, language=Kotlin)

In CBOR hex notation, the output is equivalent to the following:

text
BF                                      # map(*)
   64                                   # text(4)
      6E616D65                          # "name"
   75                                   # text(21)
      6B6F746C696E782E73657269616C697A6174696F6E # "kotlinx.serialization"
   68                                   # text(8)
      6C616E6775616765                  # "language"
   66                                   # text(6)
      4B6F746C696E                      # "Kotlin"
   FF                                   # primitive(*)

Note, CBOR as a format, unlike JSON, supports maps with non-trivial keys
(see the Allowing structured map keys section for JSON workarounds),
and Kotlin maps are serialized as CBOR maps, but some parsers (like jackson-dataformat-cbor) don't support this.

Ignoring unknown keys

CBOR format is often used to communicate with IoT devices where new properties could be added as a part of a device's
API evolution. By default, unknown keys encountered during deserialization produce an error.
This behavior can be configured with the ignoreUnknownKeys property.

kotlin
@Serializable
data class Project(val name: String)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
  val format = Cbor { ignoreUnknownKeys = true }
  
  val data = format.decodeFromHexString(
        "bf646e616d65756b6f746c696e782e73657269616c697a6174696f6e686c616e6775616765664b6f746c696eff"
    )
    println(data)
}

You can get the full code here.

It decodes the object, despite the fact that Project is missing the language property.

text
Project(name=kotlinx.serialization)

In CBOR hex notation, the input is equivalent to the following:

text
BF                                      # map(*)
   64                                   # text(4)
      6E616D65                          # "name"
   75                                   # text(21)
      6B6F746C696E782E73657269616C697A6174696F6E # "kotlinx.serialization"
   68                                   # text(8)
      6C616E6775616765                  # "language"
   66                                   # text(6)
      4B6F746C696E                      # "Kotlin"
   FF                                   # primitive(*)

Byte arrays and CBOR data types

Per the RFC 8949 Major Types section, CBOR supports the following data types:

  • Major type 0: an unsigned integer
  • Major type 1: a negative integer
  • Major type 2: a byte string
  • Major type 3: a text string
  • Major type 4: an array of data items
  • Major type 5: a map of pairs of data items
  • Major type 6: optional semantic tagging of other major types
  • Major type 7: floating-point numbers and simple data types that need no content, as well as the "break" stop code

By default, Kotlin ByteArray instances are encoded as major type 4.
When major type 2 is desired, then the @ByteString annotation can be used.
Moreover, the alwaysUseByteString configuration switch allows for globally preferring major type 2 without needing
to annotate every ByteArray in a class hierarchy.

kotlin
@Serializable
@OptIn(ExperimentalSerializationApi::class)
data class Data(
    @ByteString
    val type2: ByteArray, // CBOR Major type 2
    val type4: ByteArray  // CBOR Major type 4
)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val data = Data(byteArrayOf(1, 2, 3, 4), byteArrayOf(5, 6, 7, 8)) 
    val bytes = Cbor.encodeToByteArray(data)   
    println(bytes.toAsciiHexString())
    val obj = Cbor.decodeFromByteArray<Data>(bytes)
    println(obj)
}

You can get the full code here.

As we see, the CBOR byte that precedes the data is different for different types of encoding.

text
{BF}etype2D{01}{02}{03}{04}etype4{9F}{05}{06}{07}{08}{FF}{FF}
Data(type2=[1, 2, 3, 4], type4=[5, 6, 7, 8])

In CBOR hex notation, the output is equivalent to the following:

text
BF               # map(*)
   65            # text(5)
      7479706532 # "type2"
   44            # bytes(4)
      01020304   # "\x01\x02\x03\x04"
   65            # text(5)
      7479706534 # "type4"
   9F            # array(*)
      05         # unsigned(5)
      06         # unsigned(6)
      07         # unsigned(7)
      08         # unsigned(8)
      FF         # primitive(*)
   FF            # primitive(*)

Definite vs. Indefinite Length Encoding

CBOR supports two encodings for maps and arrays: definite and indefinite length encoding. kotlinx.serialization defaults
to the latter, which means that a map's or array's number of elements is not encoded, but instead a terminating byte is
appended after the last element.
Definite length encoding, on the other hand, omits this terminating byte, but instead prepends number of elements
to the contents of a map or array. The useDefiniteLengthEncoding configuration switch allows for toggling between the
two modes of encoding.

Tags and Labels

CBOR allows for optionally defining tags for properties and their values. These tags are encoded into the resulting
byte string to transport additional information
(see RFC 8949 Tagging of Items for more info).
The @KeyTags and @ValueTags annotations can be used to define such tags while
writing and verifying such tags can be toggled using the encodeKeyTags, encodeValueTags, verifyKeyTags, and
verifyValueTags configuration switches respectively.
In addition, it is possible to directly declare classes to always be tagged.
This then applies to all instances of such a tagged class, regardless of whether they are used as values in a list
or when they are used as a property in another class.
Forcing objects to always be tagged in such a manner is accomplished by the @ObjectTags annotation,
which works just as ValueTags, but for class definitions.
When serializing, ObjectTags will always be encoded directly before to the data of the tagged object, i.e. a
value-tagged property of an object-tagged type will have the value tags preceding the object tags.
Writing and verifying object tags can be toggled using the encodeObjectTags and verifyObjectTags configuration
switches. Note that verifying only value tags can result in some data with superfluous tags to still deserialize
successfully, since in this case - by definition - only a partial validation of tags happens.
Well-known tags are specified in CborTag.

In addition, CBOR supports keys of all types which work just as SerialNames.
COSE restricts this again to strings and numbers and calls these restricted map keys labels. String labels can be
assigned by using @SerialName, while number labels can be assigned using the @CborLabel annotation.
The preferCborLabelsOverNames configuration switch can be used to prefer number labels over SerialNames in case both
are present for a property. This duality allows for compact representation of a type when serialized to CBOR, while
keeping expressive diagnostic names when serializing to JSON.

A predefined Cbor instance (in addition to the default Cbor.Default one) is available, adhering to COSE
encoding requirements as Cbor.CoseCompliant. This instance uses definite length encoding,
encodes and verifies all tags and prefers labels to serial names.

Arrays

Classes may be serialized as a CBOR Array (major type 4) instead of a CBOR Map (major type 5).

Example usage:

text
@Serializable
data class DataClass(
    val alg: Int,
    val kid: String?
)

Cbor.encodeToByteArray(DataClass(alg = -7, kid = null))

will normally produce a Cbor map: bytes 0xa263616c6726636b6964f6, or in diagnostic notation:

text
A2           # map(2)
   63        # text(3)
      616C67 # "alg"
   26        # negative(6)
   63        # text(3)
      6B6964 # "kid"
   F6        # primitive(22)

When annotated with @CborArray, serialization of the same object will produce a Cbor array: bytes 0x8226F6, or in diagnostic notation:

text
82    # array(2)
   26 # negative(6)
   F6 # primitive(22)

This may be used to encode COSE structures, see RFC 9052 2. Basic COSE Structure.

Custom CBOR-specific Serializers

Cbor encoders and decoders implement the interfaces CborEncoder and CborDecoder, respectively.
These interfaces contain a single property, cbor, exposing the current CBOR serialization configuration.
This enables custom cbor-specific serializers to reuse the current Cbor instance to produce embedded byte arrays or
react to configuration settings such as preferCborLabelsOverNames or useDefiniteLengthEncoding, for example.

ProtoBuf (experimental)

Protocol Buffers is a language-neutral binary format that normally
relies on a separate ".proto" file that defines the protocol schema. It is more compact than CBOR, because it
assigns integer numbers to fields instead of names.

Protocol buffers support is (experimentally) available in a separate
org.jetbrains.kotlinx:kotlinx-serialization-protobuf:<version> module.

Kotlin Serialization is using proto2 semantics, where all fields are explicitly required or optional.
For a basic example we change our example to use the
ProtoBuf class with ProtoBuf.encodeToByteArray and ProtoBuf.decodeFromByteArray functions.

kotlin
@Serializable
data class Project(val name: String, val language: String)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val data = Project("kotlinx.serialization", "Kotlin") 
    val bytes = ProtoBuf.encodeToByteArray(data)   
    println(bytes.toAsciiHexString())
    val obj = ProtoBuf.decodeFromByteArray(bytes)
    println(obj)
}

You can get the full code here.

text
{0A}{15}kotlinx.serialization{12}{06}Kotlin
Project(name=kotlinx.serialization, language=Kotlin)

In ProtoBuf hex notation, the output is equivalent to the following:

text
Field #1: 0A String Length = 21, Hex = 15, UTF8 = "kotlinx.serialization"
Field #2: 12 String Length = 6, Hex = 06, UTF8 = "Kotlin"

Field numbers

By default, field numbers in the Kotlin Serialization ProtoBuf implementation are automatically assigned,
which does not provide the ability to define a stable data schema that evolves over time. That is normally achieved by
writing a separate ".proto" file. However, with Kotlin Serialization we can get this ability without a separate
schema file, instead using the ProtoNumber annotation.

kotlin
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class Project(
    @ProtoNumber(1)
    val name: String, 
    @ProtoNumber(3)
    val language: String
)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val data = Project("kotlinx.serialization", "Kotlin") 
    val bytes = ProtoBuf.encodeToByteArray(data)   
    println(bytes.toAsciiHexString())
    val obj = ProtoBuf.decodeFromByteArray(bytes)
    println(obj)
}

You can get the full code here.

We see in the output that the number for the first property name did not change (as it is numbered from one by default),
but it did change for the language property.

text
{0A}{15}kotlinx.serialization{1A}{06}Kotlin
Project(name=kotlinx.serialization, language=Kotlin)

In ProtoBuf hex notation, the output is equivalent to the following:

text
Field #1: 0A String Length = 21, Hex = 15, UTF8 = "kotlinx.serialization" (total 21 chars)
Field #3: 1A String Length = 6, Hex = 06, UTF8 = "Kotlin"

Integer types

Protocol buffers support various integer encodings optimized for different ranges of integers.
They are specified using the ProtoType annotation and the ProtoIntegerType enum.
The following example shows all three supported options.

kotlin
@OptIn(ExperimentalSerializationApi::class)
@Serializable
class Data(
    @ProtoType(ProtoIntegerType.DEFAULT)
    val a: Int,
    @ProtoType(ProtoIntegerType.SIGNED)
    val b: Int,
    @ProtoType(ProtoIntegerType.FIXED)
    val c: Int
)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val data = Data(1, -2, 3) 
    println(ProtoBuf.encodeToByteArray(data).toAsciiHexString())
}

You can get the full code here.

  • The default is a varint encoding (intXX) that is optimized for
    small non-negative numbers. The value of 1 is encoded in one byte 01.
  • The signed is a signed ZigZag encoding (sintXX) that is optimized for
    small signed integers. The value of -2 is encoded in one byte 03.
  • The fixed encoding (fixedXX) always uses a fixed number of bytes.
    The value of 3 is encoded as four bytes 03 00 00 00.

uintXX and sfixedXX protocol buffer types are not supported.

text
{08}{01}{10}{03}{1D}{03}{00}{00}{00}

In ProtoBuf hex notation the output is equivalent to the following:

text
Field #1: 08 Varint Value = 1, Hex = 01
Field #2: 10 Varint Value = 3, Hex = 03
Field #3: 1D Fixed32 Value = 3, Hex = 03-00-00-00

Lists as repeated fields

By default, kotlin lists and other collections are representend as repeated fields.
In the protocol buffers when the list is empty there are no elements in the
stream with the corresponding number. For Kotlin Serialization you must explicitly specify a default of emptyList()
for any property of a collection or map type. Otherwise you will not be able deserialize an empty
list, which is indistinguishable in protocol buffers from a missing field.

kotlin
@Serializable
data class Data(
    val a: List = emptyList(),
    val b: List = emptyList()
)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val data = Data(listOf(1, 2, 3), listOf())
    val bytes = ProtoBuf.encodeToByteArray(data)
    println(bytes.toAsciiHexString())
    println(ProtoBuf.decodeFromByteArray<Data>(bytes))
}

You can get the full code here.

text
{08}{01}{08}{02}{08}{03}
Data(a=[1, 2, 3], b=[])

In ProtoBuf diagnostic mode the output is equivalent to the following:

text
Field #1: 08 Varint Value = 1, Hex = 01
Field #1: 08 Varint Value = 2, Hex = 02
Field #1: 08 Varint Value = 3, Hex = 03

Packed fields

Collection types (not maps) can be written as packed fields when annotated with the @ProtoPacked annotation.
Per the standard packed fields can only be used on primitive numeric types. The annotation is ignored on other types.

Per the format description the parser ignores
the annotation, but rather reads list in either packed or repeated format.

Oneof field (experimental)

Kotlin Serialization ProtoBuf format supports oneof fields
basing on the Polymorphism functionality.

Usage

Given a protobuf message defined like:

proto
message Data {
    required string name = 1;
    oneof phone {
        string home_phone = 2;
        string work_phone = 3;
    }
}

You can define a kotlin class semantically equal to this message by following these steps:

  • Declare a sealed interface or abstract class, to represent of the oneof group, called the oneof interface. In our example, oneof interface is IPhoneType.
  • Declare a Kotlin class as usual to represent the whole message (class Data in our example). In this class, add the property with oneof interface type, annotated with @ProtoOneOf. Do not use @ProtoNumber for that property.
  • Declare subclasses for oneof interface, one per each oneof group element. Each class must have exactly one property with the corresponding oneof element type. In our example, these classes are HomePhone and WorkPhone.
  • Annotate properties in subclasses with @ProtoNumber, according to original oneof definition. In our example, val number: String in HomePhone has @ProtoNumber(2) annotation, because of field string home_phone = 2; in oneof phone.
kotlin
// The outer class
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class Data(
    @ProtoNumber(1) val name: String,
    @ProtoOneOf val phone: IPhoneType?,
)

// The oneof interface
@Serializable sealed interface IPhoneType

// Message holder for home_phone
@OptIn(ExperimentalSerializationApi::class)
@Serializable @JvmInline value class HomePhone(@ProtoNumber(2) val number: String): IPhoneType

// Message holder for work_phone. Can also be a value class, but we leave it as `data` to demonstrate that both variants can be used.
@OptIn(ExperimentalSerializationApi::class)
@Serializable data class WorkPhone(@ProtoNumber(3) val number: String): IPhoneType

@OptIn(ExperimentalSerializationApi::class)
fun main() {
  val dataTom = Data("Tom", HomePhone("123"))
  val stringTom = ProtoBuf.encodeToHexString(dataTom)
  val dataJerry = Data("Jerry", WorkPhone("789"))
  val stringJerry = ProtoBuf.encodeToHexString(dataJerry)
  println(stringTom)
  println(stringJerry)
  println(ProtoBuf.decodeFromHexString<Data>(stringTom))
  println(ProtoBuf.decodeFromHexString<Data>(stringJerry))
}

You can get the full code here.

text
0a03546f6d1203313233
0a054a657272791a03373839
Data(name=Tom, phone=HomePhone(number=123))
Data(name=Jerry, phone=WorkPhone(number=789))

In ProtoBuf diagnostic mode the first 2 lines in the output are equivalent to

text
Field #1: 0A String Length = 3, Hex = 03, UTF8 = "Tom" Field #2: 12 String Length = 3, Hex = 03, UTF8 = "123"
Field #1: 0A String Length = 5, Hex = 05, UTF8 = "Jerry" Field #3: 1A String Length = 3, Hex = 03, UTF8 = "789"

You should note that each group of oneof types should be tied to exactly one data class, and it is better not to reuse it in
another data class. Otherwise, you may get id conflicts or IllegalArgumentException in runtime.

Alternative

You don't always need to apply the @ProtoOneOf form in your class for messages with oneof fields, if this class is only used for deserialization.

For example, the following class:

text
@Serializable  
data class Data2(  
    @ProtoNumber(1) val name: String,  
    @ProtoNumber(2) val homeNumber: String? = null,  
    @ProtoNumber(3) val workNumber: String? = null,  
)

is also compatible with the message Data given above, which means the same input can be deserialized into it instead of Data — in case you don't want to deal with sealed hierarchies.

But please note that there are no exclusivity checks. This means that if an instance of Data2 has both (or none) homeNumber and workNumber as non-null values and is serialized to protobuf, it no longer complies with the original schema. If you send such data to another parser, one of the fields may be omitted, leading to an unknown issue.

ProtoBuf schema generator (experimental)

As mentioned above, when working with protocol buffers you usually use a ".proto" file and a code generator for your
language. This includes the code to serialize your message to an output stream and deserialize it from an input stream.
When using Kotlin Serialization this step is not necessary because your @Serializable Kotlin data types are used as the
source for the schema.

This is very convenient for Kotlin-to-Kotlin communication, but makes interoperability between languages complicated.
Fortunately, you can use the ProtoBuf schema generator to output the ".proto" representation of your messages. You can
keep your Kotlin classes as a source of truth and use traditional protoc compilers for other languages at the same time.

As an example, we can display the following data class's ".proto" schema as follows.

kotlin
@Serializable
data class SampleData(
    val amount: Long,
    val description: String?,
    val department: String = "QA"
)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
  val descriptors = listOf(SampleData.serializer().descriptor)
  val schemas = ProtoBufSchemaGenerator.generateSchemaText(descriptors)
  println(schemas)
}

You can get the full code here.

Which would output as follows.

text
syntax = "proto2";


// serial name 'example.exampleFormats09.SampleData'
message SampleData {
  required int64 amount = 1;
  optional string description = 2;
  // WARNING: a default value decoded when value is missing
  optional string department = 3;
}

Note that since default values are not represented in ".proto" files, a warning is generated when one appears in the schema.

See the documentation for ProtoBufSchemaGenerator for more information.

Properties (experimental)

Kotlin Serialization can serialize a class into a flat map with String keys via
the Properties format implementation.

Properties support is (experimentally) available in a separate
org.jetbrains.kotlinx:kotlinx-serialization-properties:<version> module.

kotlin
@Serializable
class Project(val name: String, val owner: User)

@Serializable
class User(val name: String)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val data = Project("kotlinx.serialization",  User("kotlin"))
    val map = Properties.encodeToMap(data)
    map.forEach { (k, v) -> println("$k = $v") }
}

You can get the full code here.

The resulting map has dot-separated keys representing keys of the nested objects.

text
name = kotlinx.serialization
owner.name = kotlin

Custom formats (experimental)

A custom format for Kotlin Serialization must provide an implementation for the Encoder and Decoder interfaces that
we saw used in the Serializers chapter.
These are pretty large interfaces. For convenience
the AbstractEncoder and AbstractDecoder skeleton implementations are provided to simplify the task.
In AbstractEncoder most of the encodeXxx methods have a default implementation that
delegates to encodeValue(value: Any) — the only method that must be
implemented to get a basic working format.

Basic encoder

Let us start with a trivial format implementation that encodes the data into a single list of primitive
constituent objects in the order they were written in the source code. To start, we implement a simple Encoder by
overriding encodeValue in AbstractEncoder. Since encoders are intended to be consumed by other parts of application,
it is recommended to propagate the @ExperimentalSerializationApi annotation instead of opting-in.

kotlin
@ExperimentalSerializationApi
class ListEncoder : AbstractEncoder() {
    val list = mutableListOf<Any>()

    override val serializersModule: SerializersModule = EmptySerializersModule()

    override fun encodeValue(value: Any) {
        list.add(value)
    }
}

Now we write a convenience top-level function that creates an encoder that encodes an object
and returns a list.

kotlin
@ExperimentalSerializationApi
fun <T> encodeToList(serializer: SerializationStrategy<T>, value: T): List<Any> {
    val encoder = ListEncoder()
    encoder.encodeSerializableValue(serializer, value)
    return encoder.list
}

For even more convenience, to avoid the need to explicitly pass a serializer, we write an inline overload of
the encodeToList function with a reified type parameter using the serializer function to retrieve
the appropriate KSerializer instance for the actual type.

kotlin
@ExperimentalSerializationApi
inline fun <reified T> encodeToList(value: T) = encodeToList(serializer(), value)

Now we can test it.

kotlin
@Serializable
data class Project(val name: String, val owner: User, val votes: Int)

@Serializable
data class User(val name: String)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val data = Project("kotlinx.serialization",  User("kotlin"), 9000)
    println(encodeToList(data))
}

You can get the full code here.

As a result, we got all the primitive values in our object graph visited and put into a list
in serial order.

text
[kotlinx.serialization, kotlin, 9000]

By itself, that's a useful feature if we need compute some kind of hashcode or digest for all the data
that is contained in a serializable object tree.

Basic decoder

A decoder needs to implement more substance.

  • decodeValue — returns the next value from the list.
  • decodeElementIndex — returns the next index of a deserialized value.
    In this primitive format deserialization always happens in order, so we keep track of the index
    in the elementIndex variable. See
    the Hand-written composite serializer section
    on how it ends up being used.
  • beginStructure — returns a new instance of ListDecoder, so that
    each structure that is being recursively decoded keeps track of its own elementIndex state separately.
kotlin
@ExperimentalSerializationApi
class ListDecoder(val list: ArrayDeque<Any>) : AbstractDecoder() {
    private var elementIndex = 0

    override val serializersModule: SerializersModule = EmptySerializersModule()

    override fun decodeValue(): Any = list.removeFirst()
    
    override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
        if (elementIndex == descriptor.elementsCount) return CompositeDecoder.DECODE_DONE
        return elementIndex++
    }

    override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
        ListDecoder(list)
}

A couple of convenience functions for decoding.

kotlin
@ExperimentalSerializationApi
fun <T> decodeFromList(list: List<Any>, deserializer: DeserializationStrategy<T>): T {
    val decoder = ListDecoder(ArrayDeque(list))
    return decoder.decodeSerializableValue(deserializer)
}

@ExperimentalSerializationApi
inline fun <reified T> decodeFromList(list: List<Any>): T = decodeFromList(list, serializer())

That is enough to start encoding and decoding basic serializable classes.

kotlin
@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val data = Project("kotlinx.serialization",  User("kotlin"), 9000)
    val list = encodeToList(data)
    println(list)
    val obj = decodeFromList(list)
    println(obj)
}

You can get the full code here.

Now we can convert a list of primitives back to an object tree.

text
[kotlinx.serialization, kotlin, 9000]
Project(name=kotlinx.serialization, owner=User(name=kotlin), votes=9000)

Sequential decoding

The decoder we have implemented keeps track of the elementIndex in its state and implements
decodeElementIndex. This means that it is going to work with an arbitrary serializer, even the
simple one we wrote in
the Hand-written composite serializer section.
However, this format always stores elements in order, so this bookkeeping is not needed and
undermines decoding performance. All auto-generated serializers on the JVM support
the Sequential decoding protocol (experimental), and the decoder can indicate
its support by returning true from the CompositeDecoder.decodeSequentially function.

kotlin
@ExperimentalSerializationApi
class ListDecoder(val list: ArrayDeque<Any>) : AbstractDecoder() {
    private var elementIndex = 0

    override val serializersModule: SerializersModule = EmptySerializersModule()

    override fun decodeValue(): Any = list.removeFirst()
    
    override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
        if (elementIndex == descriptor.elementsCount) return CompositeDecoder.DECODE_DONE
        return elementIndex++
    }

    override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
        ListDecoder(list) 

    override fun decodeSequentially(): Boolean = true
}

You can get the full code here.

Adding collection support

This basic format, so far, cannot properly represent collections. In encodes them, but it does not keep
track of how many elements there are in the collection or where it ends, so it cannot properly decode them.
First, let us add proper support for collections to the encoder by implementing the
Encoder.beginCollection function. The beginCollection function takes a collection size as a parameter,
so we encode it to add it to the result.
Our encoder implementation does not keep any state, so it just returns this from the beginCollection function.

kotlin
@ExperimentalSerializationApi
class ListEncoder : AbstractEncoder() {
    val list = mutableListOf<Any>()

    override val serializersModule: SerializersModule = EmptySerializersModule()

    override fun encodeValue(value: Any) {
        list.add(value)
    }                               

    override fun beginCollection(descriptor: SerialDescriptor, collectionSize: Int): CompositeEncoder {
        encodeInt(collectionSize)
        return this
    }                                                
}

The decoder, for our case, needs to only implement the CompositeDecoder.decodeCollectionSize function
in addition to the previous code.

The formats that store collection size in advance have to return true from decodeSequentially.

kotlin
@ExperimentalSerializationApi
class ListDecoder(val list: ArrayDeque<Any>, var elementsCount: Int = 0) : AbstractDecoder() {
    private var elementIndex = 0

    override val serializersModule: SerializersModule = EmptySerializersModule()

    override fun decodeValue(): Any = list.removeFirst()

    override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
        if (elementIndex == elementsCount) return CompositeDecoder.DECODE_DONE
        return elementIndex++
    }

    override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
        ListDecoder(list, descriptor.elementsCount)

    override fun decodeSequentially(): Boolean = true

    override fun decodeCollectionSize(descriptor: SerialDescriptor): Int =
        decodeInt().also { elementsCount = it }
}

That is all that is needed to support collections and maps.

kotlin
@Serializable
data class Project(val name: String, val owners: List, val votes: Int)

@Serializable
data class User(val name: String)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val data = Project("kotlinx.serialization",  listOf(User("kotlin"), User("jetbrains")), 9000)
    val list = encodeToList(data)
    println(list)
    val obj = decodeFromList(list)
    println(obj)
}

You can get the full code here.

We see the size of the list added to the result, letting the decoder know where to stop.

text
[kotlinx.serialization, 2, kotlin, jetbrains, 9000]
Project(name=kotlinx.serialization, owners=[User(name=kotlin), User(name=jetbrains)], votes=9000)

Adding null support

Our trivial format does not support null values so far. For nullable types we need to add some kind
of "null indicator", telling whether the upcoming value is null or not.

In the encoder implementation we override Encoder.encodeNull and Encoder.encodeNotNullMark.

kotlin
override fun encodeNull() = encodeValue("NULL")
    override fun encodeNotNullMark() = encodeValue("!!")

In the decoder implementation we override Decoder.decodeNotNullMark.

kotlin
override fun decodeNotNullMark(): Boolean = decodeString() != "NULL"

Let us test nullable properties both with not-null and null values.

kotlin
@Serializable
data class Project(val name: String, val owner: User?, val votes: Int?)

@Serializable
data class User(val name: String)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val data = Project("kotlinx.serialization",  User("kotlin") , null)
    val list = encodeToList(data)
    println(list)
    val obj = decodeFromList(list)
    println(obj)
}

You can get the full code here.

In the output we see how not-null!! and NULL marks are used.

text
[kotlinx.serialization, !!, kotlin, NULL]
Project(name=kotlinx.serialization, owner=User(name=kotlin), votes=null)

Efficient binary format

Now we are ready for an example of an efficient binary format. We are going to write data to the
java.io.DataOutput implementation. Instead of encodeValue we must override the individual
encodeXxx functions for each of ten primitives in the encoder.

kotlin
@ExperimentalSerializationApi
class DataOutputEncoder(val output: DataOutput) : AbstractEncoder() {
    override val serializersModule: SerializersModule = EmptySerializersModule()
    override fun encodeBoolean(value: Boolean) = output.writeByte(if (value) 1 else 0)
    override fun encodeByte(value: Byte) = output.writeByte(value.toInt())
    override fun encodeShort(value: Short) = output.writeShort(value.toInt())
    override fun encodeInt(value: Int) = output.writeInt(value)
    override fun encodeLong(value: Long) = output.writeLong(value)
    override fun encodeFloat(value: Float) = output.writeFloat(value)
    override fun encodeDouble(value: Double) = output.writeDouble(value)
    override fun encodeChar(value: Char) = output.writeChar(value.code)
    override fun encodeString(value: String) = output.writeUTF(value)
    override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = output.writeInt(index)

    override fun beginCollection(descriptor: SerialDescriptor, collectionSize: Int): CompositeEncoder {
        encodeInt(collectionSize)
        return this
    }

    override fun encodeNull() = encodeBoolean(false)
    override fun encodeNotNullMark() = encodeBoolean(true)
}

The decoder implementation mirrors encoder's implementation overriding all the primitive decodeXxx functions.

kotlin
@ExperimentalSerializationApi
class DataInputDecoder(val input: DataInput, var elementsCount: Int = 0) : AbstractDecoder() {
    private var elementIndex = 0
    override val serializersModule: SerializersModule = EmptySerializersModule()
    override fun decodeBoolean(): Boolean = input.readByte().toInt() != 0
    override fun decodeByte(): Byte = input.readByte()
    override fun decodeShort(): Short = input.readShort()
    override fun decodeInt(): Int = input.readInt()
    override fun decodeLong(): Long = input.readLong()
    override fun decodeFloat(): Float = input.readFloat()
    override fun decodeDouble(): Double = input.readDouble()
    override fun decodeChar(): Char = input.readChar()
    override fun decodeString(): String = input.readUTF()
    override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = input.readInt()

    override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
        if (elementIndex == elementsCount) return CompositeDecoder.DECODE_DONE
        return elementIndex++
    }

    override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
        DataInputDecoder(input, descriptor.elementsCount)

    override fun decodeSequentially(): Boolean = true

    override fun decodeCollectionSize(descriptor: SerialDescriptor): Int =
        decodeInt().also { elementsCount = it }

    override fun decodeNotNullMark(): Boolean = decodeBoolean()
}

We can now serialize and deserialize arbitrary data. For example, the same classes as were
used in the CBOR (experimental) and ProtoBuf (experimental) sections.

kotlin
@Serializable
data class Project(val name: String, val language: String)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val data = Project("kotlinx.serialization", "Kotlin")
    val output = ByteArrayOutputStream()
    encodeTo(DataOutputStream(output), data)
    val bytes = output.toByteArray()
    println(bytes.toAsciiHexString())
    val input = ByteArrayInputStream(bytes)
    val obj = decodeFrom(DataInputStream(input))
    println(obj)
}

You can get the full code here.

As we can see, the result is a dense binary format that only contains the data that is being serialized.
It can be easily tweaked for any kind of domain-specific compact encoding.

text
{00}{15}kotlinx.serialization{00}{06}Kotlin
Project(name=kotlinx.serialization, language=Kotlin)

Format-specific types

A format implementation might provide special support for data types that are not among the list of primitive
types in Kotlin Serialization, and do not have a corresponding encodeXxx/decodeXxx function.
In the encoder this is achieved by overriding the
encodeSerializableValue(serializer, value) function.

In our DataOutput format example we might want to provide a specialized efficient data path for serializing an array
of bytes since DataOutput has a special method for this purpose.

Detection of the type is performed by looking at the serializer.descriptor, not by checking the type of the value
being serialized, so we fetch the builtin KSerializer instance for ByteArray type.

This an important difference. This way our format implementation properly supports
Custom serializers that a user might specify for a type that just happens
to be internally represented as a byte array, but need a different serial representation.

kotlin
private val byteArraySerializer = serializer()

Specifically for byte arrays, we could have also used the builtin
ByteArraySerializer function.

We add the corresponding code to the Encoder implementation of our
Efficient binary format. To make our ByteArray encoding even more efficient,
we add a trivial implementation of encodeCompactSize function that uses only one byte to represent
a size of up to 254 bytes.

kotlin
override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
        if (serializer.descriptor == byteArraySerializer.descriptor)
            encodeByteArray(value as ByteArray)
        else
            super.encodeSerializableValue(serializer, value)
    }

    private fun encodeByteArray(bytes: ByteArray) {
        encodeCompactSize(bytes.size)
        output.write(bytes)
    }
    
    private fun encodeCompactSize(value: Int) {
        if (value < 0xff) {
            output.writeByte(value)
        } else {
            output.writeByte(0xff)
            output.writeInt(value)
        }
    }

A similar code is added to the Decoder implementation. Here we override
the decodeSerializableValue function.

kotlin
@Suppress("UNCHECKED_CAST")
    override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>, previousValue: T?): T =
        if (deserializer.descriptor == byteArraySerializer.descriptor)
            decodeByteArray() as T
        else
            super.decodeSerializableValue(deserializer, previousValue)

    private fun decodeByteArray(): ByteArray {
        val bytes = ByteArray(decodeCompactSize())
        input.readFully(bytes)
        return bytes
    }

    private fun decodeCompactSize(): Int {
        val byte = input.readByte().toInt() and 0xff
        if (byte < 0xff) return byte
        return input.readInt()
    }

Now everything is ready to perform serialization of some byte arrays.

kotlin
@Serializable
data class Project(val name: String, val attachment: ByteArray)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val data = Project("kotlinx.serialization", byteArrayOf(0x0A, 0x0B, 0x0C, 0x0D))
    val output = ByteArrayOutputStream()
    encodeTo(DataOutputStream(output), data)
    val bytes = output.toByteArray()
    println(bytes.toAsciiHexString())
    val input = ByteArrayInputStream(bytes)
    val obj = decodeFrom(DataInputStream(input))
    println(obj)
}

You can get the full code here.

As we can see, our custom byte array format is being used, with the compact encoding of its size in one byte.

text
{00}{15}kotlinx.serialization{04}{0A}{0B}{0C}{0D}
Project(name=kotlinx.serialization, attachment=[10, 11, 12, 13])

This chapter concludes Kotlin Serialization Guide.


File: docs/inline-classes.md

The documentation has been moved to the value-classes.md page.


File: docs/json.md

JSON features

This is the fifth chapter of the Kotlin Serialization Guide.
In this chapter, we'll walk through features of JSON serialization available in the Json class.

Table of contents

Json configuration

The default Json implementation is quite strict with respect to invalid inputs. It enforces Kotlin type safety and
restricts Kotlin values that can be serialized so that the resulting JSON representations are standard.
Many non-standard JSON features are supported by creating a custom instance of a JSON format.

To use a custom JSON format configuration, create your own Json class instance from an existing
instance, such as a default Json object, using the Json() builder function. Specify parameter values
in the parentheses via the JsonBuilder DSL. The resulting Json format instance is immutable and thread-safe;
it can be simply stored in a top-level property.

We recommend that you store and reuse custom instances of formats for performance reasons because format implementations
may cache format-specific additional information about the classes they serialize.

This chapter shows configuration features that Json supports.

Pretty printing

By default, the Json output is a single line. You can configure it to pretty print the output (that is, add indentations
and line breaks for better readability) by setting the prettyPrint property to true:

kotlin
val format = Json { prettyPrint = true }

@Serializable
data class Project(val name: String, val language: String)

fun main() {
    val data = Project("kotlinx.serialization", "Kotlin")
    println(format.encodeToString(data))
}

You can get the full code here.

It gives the following nice result:

text
{
    "name": "kotlinx.serialization",
    "language": "Kotlin"
}

Lenient parsing

By default, Json parser enforces various JSON restrictions to be as specification-compliant as possible
(see RFC-4627). Particularly, keys and string literals must be quoted. Those restrictions can be relaxed with
the isLenient property. With isLenient = true, you can parse quite freely-formatted data:

kotlin
val format = Json { isLenient = true }

enum class Status { SUPPORTED }

@Serializable
data class Project(val name: String, val status: Status, val votes: Int)

fun main() {
    val data = format.decodeFromString("""
        {
            name   : kotlinx.serialization,
            status : SUPPORTED,
            votes  : "9000"
        }
    """)
    println(data)
}

You can get the full code here.

You get the object, even though all keys of the source JSON, string and enum values are unquoted:

text
Project(name=kotlinx.serialization, status=SUPPORTED, votes=9000)

Note that parsing of quoted numbers or booleans such as votes: "9000" to val votes: Int is generally allowed by kotlinx.serialization
regardless of the isLenient flag, since such JSON is syntactically valid.

Ignoring unknown keys

JSON format is often used to read the output of third-party services or in other dynamic environments where
new properties can be added during the API evolution. By default, unknown keys encountered during deserialization produce an error.
You can avoid this and just ignore such keys by setting the ignoreUnknownKeys property
to true:

kotlin
val format = Json { ignoreUnknownKeys = true }

@Serializable
data class Project(val name: String)

fun main() {
    val data = format.decodeFromString("""
        {"name":"kotlinx.serialization","language":"Kotlin"}
    """)
    println(data)
}

You can get the full code here.

It decodes the object despite the fact that the Project class doesn't have the language property:

text
Project(name=kotlinx.serialization)

Ignoring unknown keys per class

Sometimes, for cleaner and safer API, it is desirable to ignore unknown properties only for specific classes.
In that case, you can use JsonIgnoreUnknownKeys annotation on such classes while leaving global ignoreUnknownKeys setting
turned off:

kotlin
@OptIn(ExperimentalSerializationApi::class) // JsonIgnoreUnknownKeys is an experimental annotation for now
@Serializable
@JsonIgnoreUnknownKeys
data class Outer(val a: Int, val inner: Inner)

@Serializable
data class Inner(val x: String)

fun main() {
    // 1
    println(Json.decodeFromString<Outer>("""{"a":1,"inner":{"x":"value"},"unknownKey":42}"""))
    println()
    // 2
    println(Json.decodeFromString<Outer>("""{"a":1,"inner":{"x":"value","unknownKey":"unknownValue"}}"""))
}

You can get the full code here.

Line (1) decodes successfully despite "unknownKey" in Outer, because annotation is present on the class.
However, line (2) throws SerializationException because there is no "unknownKey" property in Inner:

text
Outer(a=1, inner=Inner(x=value))

Exception in thread "main" kotlinx.serialization.json.JsonDecodingException: Unexpected JSON token at offset 29: Encountered an unknown key 'unknownKey' at path: $.inner
Use 'ignoreUnknownKeys = true' in 'Json {}' builder or '@JsonIgnoreUnknownKeys' annotation to ignore unknown keys.

Alternative Json names

It's not a rare case when JSON fields are renamed due to a schema version change.
You can use the @SerialName annotation to change the name of a JSON field,
but such renaming blocks the ability to decode data with the old name.
To support multiple JSON names for the one Kotlin property, there is the JsonNames annotation:

kotlin
@OptIn(ExperimentalSerializationApi::class) // JsonNames is an experimental annotation for now
@Serializable
data class Project(@JsonNames("title") val name: String)

fun main() {
  val project = Json.decodeFromString("""{"name":"kotlinx.serialization"}""")
  println(project)
  val oldProject = Json.decodeFromString("""{"title":"kotlinx.coroutines"}""")
  println(oldProject)
}

You can get the full code here.

As you can see, both name and title Json fields correspond to name property:

text
Project(name=kotlinx.serialization)
Project(name=kotlinx.coroutines)

Support for JsonNames annotation is controlled by the JsonBuilder.useAlternativeNames flag.
Unlike most of the configuration flags, this one is enabled by default and does not need attention
unless you want to do some fine-tuning.

Encoding defaults

Default values of properties are not encoded by default because they will be assigned to missing fields during decoding anyway.
See the Defaults are not encoded section for details and an example.
This is especially useful for nullable properties with null defaults and avoids writing the corresponding null values.
The default behavior can be changed by setting the encodeDefaults property to true:

kotlin
val format = Json { encodeDefaults = true }

@Serializable
class Project(
    val name: String,
    val language: String = "Kotlin",
    val website: String? = null
)

fun main() {
    val data = Project("kotlinx.serialization")
    println(format.encodeToString(data))
}

You can get the full code here.

It produces the following output which encodes all the property values including the default ones:

text
{"name":"kotlinx.serialization","language":"Kotlin","website":null}

Explicit nulls

By default, all null values are encoded into JSON strings, but in some cases you may want to omit them.
The encoding of null values can be controlled with the explicitNulls property.

If you set property to false, fields with null values are not encoded into JSON even if the property does not have a
default null value. When decoding such JSON, the absence of a property value is treated as null for nullable properties
without a default value.

kotlin
val format = Json { explicitNulls = false }

@Serializable
data class Project(
    val name: String,
    val language: String,
    val version: String? = "1.2.2",
    val website: String?,
    val description: String? = null
)

fun main() {
    val data = Project("kotlinx.serialization", "Kotlin", null, null, null)
    val json = format.encodeToString(data)
    println(json)
    println(format.decodeFromString(json))
}

You can get the full code here.

As you can see, version, website and description fields are not present in output JSON on the first line.
After decoding, the missing nullable property website without a default values has received a null value,
while nullable properties version and description are filled with their default values:

text
{"name":"kotlinx.serialization","language":"Kotlin"}
Project(name=kotlinx.serialization, language=Kotlin, version=1.2.2, website=null, description=null)

Pay attention to the fact that version was null before encoding and became 1.2.2 after decoding.
Encoding/decoding of properties like this — nullable with a non-null default — becomes asymmetrical if explicitNulls is set to false.

It is possible to make the decoder treat some invalid input data as a missing field to enhance the functionality of this flag.
See coerceInputValues below for details.

explicitNulls is true by default as it is the default behavior across different versions of the library.

Coercing input values

JSON formats that from third parties can evolve, sometimes changing the field types.
This can lead to exceptions during decoding when the actual values do not match the expected values.
The default Json implementation is strict with respect to input types as was demonstrated in
the Type safety is enforced section. You can relax this restriction
using the coerceInputValues property.

This property only affects decoding. It treats a limited subset of invalid input values as if the
corresponding property was missing.
The current list of supported invalid values is:

  • null inputs for non-nullable types
  • unknown values for enums

If value is missing, it is replaced either with a default property value if it exists,
or with a null if explicitNulls flag is set to false and a property is nullable (for enums).

This list may be expanded in the future, so that Json instance configured with this property becomes even more
permissive to invalid value in the input, replacing them with defaults or nulls.

See the example from the Type safety is enforced section:

kotlin
val format = Json { coerceInputValues = true }

@Serializable
data class Project(val name: String, val language: String = "Kotlin")

fun main() {
    val data = format.decodeFromString("""
        {"name":"kotlinx.serialization","language":null}
    """)
    println(data)
}

You can get the full code here.

The invalid null value for the language property was coerced into the default value:

text
Project(name=kotlinx.serialization, language=Kotlin)

Example of using this flag together with explicitNulls to coerce invalid enum values:

kotlin
enum class Color { BLACK, WHITE }

@Serializable
data class Brush(val foreground: Color = Color.BLACK, val background: Color?)

val json = Json { 
  coerceInputValues = true
  explicitNulls = false
}

fun main() {
    val brush = json.decodeFromString("""{"foreground":"pink", "background":"purple"}""")
  println(brush)
}

You can get the full code here.

Despite that we do not have Color.pink and Color.purple colors, decodeFromString function returns successfully:

text
Brush(foreground=BLACK, background=null)

foreground property received its default value, and background property received null because of explicitNulls = false setting.

Allowing structured map keys

JSON format does not natively support the concept of a map with structured keys. Keys in JSON objects
are strings and can be used to represent only primitives or enums by default.
You can enable non-standard support for structured keys with
the allowStructuredMapKeys property.

This is how you can serialize a map with keys of a user-defined class:

kotlin
val format = Json { allowStructuredMapKeys = true }

@Serializable
data class Project(val name: String)

fun main() {
    val map = mapOf(
        Project("kotlinx.serialization") to "Serialization",
        Project("kotlinx.coroutines") to "Coroutines"
    )
    println(format.encodeToString(map))
}

You can get the full code here.

The map with structured keys gets represented as JSON array with the following items: [key1, value1, key2, value2,...].

text
[{"name":"kotlinx.serialization"},"Serialization",{"name":"kotlinx.coroutines"},"Coroutines"]

Allowing special floating-point values

By default, special floating-point values like Double.NaN and infinities are not supported in JSON because
the JSON specification prohibits it.
You can enable their encoding using the allowSpecialFloatingPointValues
property:

kotlin
val format = Json { allowSpecialFloatingPointValues = true }

@Serializable
class Data(
    val value: Double
)

fun main() {
    val data = Data(Double.NaN)
    println(format.encodeToString(data))
}

You can get the full code here.

This example produces the following non-stardard JSON output, yet it is a widely used encoding for
special values in JVM world:

text
{"value":NaN}

Class discriminator for polymorphism

A key name that specifies a type when you have a polymorphic data can be specified
in the classDiscriminator property:

kotlin
val format = Json { classDiscriminator = "#class" }

@Serializable
sealed class Project {
    abstract val name: String
}

@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(data))
}

You can get the full code here.

In combination with an explicitly specified SerialName of the class it provides full
control over the resulting JSON object:

text
{"#class":"owned","name":"kotlinx.coroutines","owner":"kotlin"}

It is also possible to specify different class discriminators for different hierarchies. Instead of Json instance property, use JsonClassDiscriminator annotation directly on base serializable class:

kotlin
@OptIn(ExperimentalSerializationApi::class) // JsonClassDiscriminator is an experimental annotation for now
@Serializable
@JsonClassDiscriminator("message_type")
sealed class Base

This annotation is inheritable, so all subclasses of Base will have the same discriminator:

kotlin
@Serializable // Class discriminator is inherited from Base
sealed class ErrorClass: Base()

To learn more about inheritable serial annotations, see documentation for InheritableSerialInfo.

Note that it is not possible to explicitly specify different class discriminators in subclasses of Base. Only hierarchies with empty intersections can have different discriminators.

Discriminator specified in the annotation has priority over discriminator in Json configuration:

kotlin
val format = Json { classDiscriminator = "#class" }

fun main() {
    val data = Message(BaseMessage("not found"), GenericError(404))
    println(format.encodeToString(data))
}

You can get the full code here.

As you can see, discriminator from the Base class is used:

text
{"message":{"message_type":"my.app.BaseMessage","message":"not found"},"error":{"message_type":"my.app.GenericError","error_code":404}}

Class discriminator output mode

Class discriminator provides information for serializing and deserializing polymorphic class hierarchies.
As shown above, it is only added for polymorphic classes by default.
In case you want to encode more or less information for various third party APIs about types in the output, it is possible to control
addition of the class discriminator with the JsonBuilder.classDiscriminatorMode property.

For example, ClassDiscriminatorMode.NONE does not add class discriminator at all, in case the receiving party is not interested in Kotlin types:

kotlin
@OptIn(ExperimentalSerializationApi::class) // classDiscriminatorMode is an experimental setting for now
val format = Json { classDiscriminatorMode = ClassDiscriminatorMode.NONE }

@Serializable
sealed class Project {
    abstract val name: String
}

@Serializable
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(data))
}

You can get the full code here.

Note that it would be impossible to deserialize this output back with kotlinx.serialization.

text
{"name":"kotlinx.coroutines","owner":"kotlin"}

Two other available values are ClassDiscriminatorMode.POLYMORPHIC (default behavior) and ClassDiscriminatorMode.ALL_JSON_OBJECTS (adds discriminator whenever possible).
Consult their documentation for details.

Decoding enums in a case-insensitive manner

Kotlin's naming policy recommends naming enum values
using either uppercase underscore-separated names or upper camel case names.
Json uses exact Kotlin enum values names for decoding by default.
However, sometimes third-party JSONs have such values named in lowercase or some mixed case.
In this case, it is possible to decode enum values in a case-insensitive manner using JsonBuilder.decodeEnumsCaseInsensitive property:

kotlin
@OptIn(ExperimentalSerializationApi::class) // decodeEnumsCaseInsensitive is an experimental setting for now
val format = Json { decodeEnumsCaseInsensitive = true }

@OptIn(ExperimentalSerializationApi::class) // JsonNames is an experimental annotation for now
enum class Cases { VALUE_A, @JsonNames("Alternative") VALUE_B }

@Serializable
data class CasesList(val cases: List<Cases>)

fun main() {
  println(format.decodeFromString<CasesList>("""{"cases":["value_A", "alternative"]}""")) 
}

You can get the full code here.

It affects serial names as well as alternative names specified with JsonNames annotation, so both values are successfully decoded:

text
CasesList(cases=[VALUE_A, VALUE_B])

This property does not affect encoding in any way.

Global naming strategy

If properties' names in Json input are different from Kotlin ones, it is recommended to specify the name
for each property explicitly using @SerialName annotation.
However, there are certain situations where transformation should be applied to every serial name — such as migration
from other frameworks or legacy codebase. For that cases, it is possible to specify a namingStrategy
for a Json instance. kotlinx.serialization provides one strategy implementation out of the box, the JsonNamingStrategy.SnakeCase:

kotlin
@Serializable
data class Project(val projectName: String, val projectOwner: String)

@OptIn(ExperimentalSerializationApi::class) // namingStrategy is an experimental setting for now
val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase }

fun main() {
    val project = format.decodeFromString("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""")
    println(format.encodeToString(project.copy(projectName = "kotlinx.serialization")))
}

You can get the full code here.

As you can see, both serialization and deserialization work as if all serial names are transformed from camel case to snake case:

text
{"project_name":"kotlinx.serialization","project_owner":"Kotlin"}

There are some caveats one should remember while dealing with a JsonNamingStrategy:

  • Due to the nature of the kotlinx.serialization framework, naming strategy transformation is applied to all properties regardless
    of whether their serial name was taken from the property name or provided by SerialName annotation.
    Effectively, it means one cannot avoid transformation by explicitly specifying the serial name. To be able to deserialize
    non-transformed names, JsonNames annotation can be used instead.

  • Collision of the transformed name with any other (transformed) properties serial names or any alternative names
    specified with JsonNames will lead to a deserialization exception.

  • Global naming strategies are very implicit: by looking only at the definition of the class,
    it is impossible to determine which names it will have in the serialized form.
    As a consequence, naming strategies are not friendly to actions like Find Usages/Rename in IDE, full-text search by grep, etc.
    For them, the original name and the transformed are two different things;
    changing one without the other may introduce bugs in many unexpected ways and lead to greater maintenance efforts for code with global naming strategies.

Therefore, one should carefully weigh the pros and cons before considering adding global naming strategies to an application.

Base64

To encode and decode Base64 formats, we will need to manually write a serializer. Here, we will use a default
implementation of Kotlin's Base64 encoder. Note that some serializers use different RFCs for Base64 encoding by default.
For example, Jackson uses a variant of Base64 Mime. The same result in
kotlinx.serialization can be achieved with Base64.Mime encoder.
Kotlin's documentation for Base64 lists
other available encoders.

kotlin
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.descriptors.*
import kotlin.io.encoding.*

@OptIn(ExperimentalEncodingApi::class)
object ByteArrayAsBase64Serializer : KSerializer {
    private val base64 = Base64.Default

    override val descriptor: SerialDescriptor
        get() = PrimitiveSerialDescriptor(
            "ByteArrayAsBase64Serializer",
            PrimitiveKind.STRING
        )

    override fun serialize(encoder: Encoder, value: ByteArray) {
        val base64Encoded = base64.encode(value)
        encoder.encodeString(base64Encoded)
    }

    override fun deserialize(decoder: Decoder): ByteArray {
        val base64Decoded = decoder.decodeString()
        return base64.decode(base64Decoded)
    }
}

For more details on how to create your own custom serializer, you can
see custom serializers.

Then we can use it like this:

kotlin
@Serializable
data class Value(
    @Serializable(with = ByteArrayAsBase64Serializer::class)
    val base64Input: ByteArray
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false
        other as Value
        return base64Input.contentEquals(other.base64Input)
    }

    override fun hashCode(): Int {
        return base64Input.contentHashCode()
    }
}

fun main() {
    val string = "foo string"
    val value = Value(string.toByteArray())
    val encoded = Json.encodeToString(value)
    println(encoded)
    val decoded = Json.decodeFromString<Value>(encoded)
    println(decoded.base64Input.decodeToString())
}

You can get the full code here

text
{"base64Input":"Zm9vIHN0cmluZw=="}
foo string

Notice the serializer we wrote is not dependent on Json format, therefore, it can be used in any format.

For projects that use this serializer in many places, to avoid specifying the serializer every time, it is possible
to specify a serializer globally using typealias.
For example:

kotlin
typealias Base64ByteArray = @Serializable(ByteArrayAsBase64Serializer::class) ByteArray

Json elements

Aside from direct conversions between strings and JSON objects, Kotlin serialization offers APIs that allow
other ways of working with JSON in the code. For example, you might need to tweak the data before it can parse
or otherwise work with such an unstructured data that it does not readily fit into the typesafe world of Kotlin
serialization.

The main concept in this part of the library is JsonElement. Read on to learn what you can do with it.

Parsing to Json element

A string can be parsed into an instance of JsonElement with the Json.parseToJsonElement function.
It is called neither decoding nor deserialization because none of that happens in the process.
It just parses a JSON and forms an object representing it:

kotlin
fun main() {
    val element = Json.parseToJsonElement("""
        {"name":"kotlinx.serialization","language":"Kotlin"}
    """)
    println(element)
}

You can get the full code here.

A JsonElement prints itself as a valid JSON:

text
{"name":"kotlinx.serialization","language":"Kotlin"}

Types of Json elements

A JsonElement class has three direct subtypes, closely following JSON grammar:

  • JsonPrimitive represents primitive JSON elements, such as string, number, boolean, and null.
    Each primitive has a simple string content. There is also a
    JsonPrimitive() constructor function overloaded to accept various primitive Kotlin types and
    to convert them to JsonPrimitive.

  • JsonArray represents a JSON [...] array. It is a Kotlin List of JsonElement items.

  • JsonObject represents a JSON {...} object. It is a Kotlin Map from String keys to JsonElement values.

The JsonElement class has extensions that cast it to its corresponding subtypes:
jsonPrimitive, jsonArray, jsonObject. The JsonPrimitive class,
in turn, provides converters to Kotlin primitive types: int, intOrNull, long, longOrNull,
and similar ones for other types. This is how you can use them for processing JSON whose structure you know:

kotlin
fun main() {
    val element = Json.parseToJsonElement("""
        {
            "name": "kotlinx.serialization",
            "forks": [{"votes": 42}, {"votes": 9000}, {}]
        }
    """)
    val sum = element
        .jsonObject["forks"]!!
        .jsonArray.sumOf { it.jsonObject["votes"]?.jsonPrimitive?.int ?: 0 }
    println(sum)
}

You can get the full code here.

The above example sums votes in all objects in the forks array, ignoring the objects that have no votes:

text
9042

Note that the execution will fail if the structure of the data is otherwise different.

Json element builders

You can construct instances of specific JsonElement subtypes using the respective builder functions
buildJsonArray and buildJsonObject. They provide a DSL to define the resulting JSON structure. It
is similar to Kotlin standard library collection builders, but with a JSON-specific convenience
of more type-specific overloads and inner builder functions. The following example shows
all the key features:

kotlin
fun main() {
    val element = buildJsonObject {
        put("name", "kotlinx.serialization")
        putJsonObject("owner") {
            put("name", "kotlin")
        }
        putJsonArray("forks") {
            addJsonObject {
                put("votes", 42)
            }
            addJsonObject {
                put("votes", 9000)
            }
        }
    }
    println(element)
}

You can get the full code here.

As a result, you get a proper JSON string:

text
{"name":"kotlinx.serialization","owner":{"name":"kotlin"},"forks":[{"votes":42},{"votes":9000}]}

Decoding Json elements

An instance of the JsonElement class can be decoded into a serializable object using
the Json.decodeFromJsonElement function:

kotlin
@Serializable
data class Project(val name: String, val language: String)

fun main() {
    val element = buildJsonObject {
        put("name", "kotlinx.serialization")
        put("language", "Kotlin")
    }
    val data = Json.decodeFromJsonElement(element)
    println(data)
}

You can get the full code here.

The result is exactly what you would expect:

text
Project(name=kotlinx.serialization, language=Kotlin)

Encoding literal Json content

In some cases it might be necessary to encode an arbitrary unquoted value.
This can be achieved with JsonUnquotedLiteral.

Serializing large decimal numbers

The JSON specification does not restrict the size or precision of numbers, however it is not possible to serialize
numbers of arbitrary size or precision using JsonPrimitive().

If Double is used, then the numbers are limited in precision, meaning that large numbers are truncated.
When using Kotlin/JVM BigDecimal can be used instead, but JsonPrimitive() will encode the value as a string, not a
number.

kotlin
import java.math.BigDecimal

val format = Json { prettyPrint = true }

fun main() {
    val pi = BigDecimal("3.141592653589793238462643383279")
    
    val piJsonDouble = JsonPrimitive(pi.toDouble())
    val piJsonString = JsonPrimitive(pi.toString())
  
    val piObject = buildJsonObject {
        put("pi_double", piJsonDouble)
        put("pi_string", piJsonString)
    }

    println(format.encodeToString(piObject))
}

You can get the full code here.

Even though pi was defined as a number with 30 decimal places, the resulting JSON does not reflect this.
The Double value is truncated to 15 decimal places, and the String is wrapped in quotes - which is not a JSON number.

text
{
    "pi_double": 3.141592653589793,
    "pi_string": "3.141592653589793238462643383279"
}

To avoid precision loss, the string value of pi can be encoded using JsonUnquotedLiteral.

kotlin
import java.math.BigDecimal

val format = Json { prettyPrint = true }

fun main() {
    val pi = BigDecimal("3.141592653589793238462643383279")

    // use JsonUnquotedLiteral to encode raw JSON content
    val piJsonLiteral = JsonUnquotedLiteral(pi.toString())

    val piJsonDouble = JsonPrimitive(pi.toDouble())
    val piJsonString = JsonPrimitive(pi.toString())
  
    val piObject = buildJsonObject {
        put("pi_literal", piJsonLiteral)
        put("pi_double", piJsonDouble)
        put("pi_string", piJsonString)
    }

    println(format.encodeToString(piObject))
}

You can get the full code here.

pi_literal now accurately matches the value defined.

text
{
    "pi_literal": 3.141592653589793238462643383279,
    "pi_double": 3.141592653589793,
    "pi_string": "3.141592653589793238462643383279"
}

To decode pi back to a BigDecimal, the string content of the JsonPrimitive can be used.

(This demonstration uses a JsonPrimitive for simplicity. For a more re-usable method of handling serialization, see
Json Transformations below.)

kotlin
import java.math.BigDecimal

fun main() {
    val piObjectJson = """
          {
              "pi_literal": 3.141592653589793238462643383279
          }
      """.trimIndent()
    
    val piObject: JsonObject = Json.decodeFromString(piObjectJson)
    
    val piJsonLiteral = piObject["pi_literal"]!!.jsonPrimitive.content
    
    val pi = BigDecimal(piJsonLiteral)
    
    println(pi)
}

You can get the full code here.

The exact value of pi is decoded, with all 30 decimal places of precision that were in the source JSON.

text
3.141592653589793238462643383279

Using `JsonUnquotedLiteral` to create a literal unquoted value of `null` is forbidden

To avoid creating an inconsistent state, encoding a String equal to "null" is forbidden.
Use JsonNull or JsonPrimitive instead.

kotlin
fun main() {
    // caution: creating null with JsonUnquotedLiteral will cause an exception! 
    JsonUnquotedLiteral("null")
}

You can get the full code here.

text
Exception in thread "main" kotlinx.serialization.json.JsonEncodingException: Creating a literal unquoted value of 'null' is forbidden.

Json transformations

To affect the shape and contents of JSON output after serialization, or adapt input to deserialization,
it is possible to write a custom serializer. However, it may be inconvenient to
carefully follow Encoder and Decoder calling conventions, especially for relatively small and easy tasks.
For that purpose, Kotlin serialization provides an API that can reduce the burden of implementing a custom
serializer to a problem of manipulating a Json elements tree.

We recommend that you get familiar with the Serializers chapter: among other things, it
explains how custom serializers are bound to classes.

Transformation capabilities are provided by the abstract JsonTransformingSerializer class which implements KSerializer.
Instead of direct interaction with Encoder or Decoder, this class asks you to supply transformations for JSON tree
represented by the JsonElement class using thetransformSerialize and
transformDeserialize methods. Let's take a look at the examples.

Array wrapping

The first example is an implementation of JSON array wrapping for lists.

Consider a REST API that returns a JSON array of User objects, or a single object (not wrapped into an array) if there
is only one element in the result.

In the data model, use the @Serializable annotation to specify a custom serializer for a
users: List property.

kotlin
@Serializable
data class Project(
    val name: String,
    @Serializable(with = UserListSerializer::class)
    val users: List
)

@Serializable
data class User(val name: String)

Since this example covers only the deserialization case, you can implement UserListSerializer and override only the
transformDeserialize function. The JsonTransformingSerializer constructor takes an original serializer
as parameter (this approach is shown in the section Constructing collection serializers):

kotlin
object UserListSerializer : JsonTransformingSerializer<List>(ListSerializer(User.serializer())) {
    // If response is not an array, then it is a single object that should be wrapped into the array
    override fun transformDeserialize(element: JsonElement): JsonElement =
        if (element !is JsonArray) JsonArray(listOf(element)) else element
}

Now you can test the code with a JSON array or a single JSON object as inputs.

kotlin
fun main() {
    println(Json.decodeFromString("""
        {"name":"kotlinx.serialization","users":{"name":"kotlin"}}
    """))
    println(Json.decodeFromString("""
        {"name":"kotlinx.serialization","users":[{"name":"kotlin"},{"name":"jetbrains"}]}
    """))
}

You can get the full code here.

The output shows that both cases are correctly deserialized into a Kotlin List.

text
Project(name=kotlinx.serialization, users=[User(name=kotlin)])
Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])

Array unwrapping

You can also implement the transformSerialize function to unwrap a single-element list into a single JSON object
during serialization:

kotlin
override fun transformSerialize(element: JsonElement): JsonElement {
        require(element is JsonArray) // this serializer is used only with lists
        return element.singleOrNull() ?: element
    }

Now, if you serialize a single-element list of objects from Kotlin:

kotlin
fun main() {
    val data = Project("kotlinx.serialization", listOf(User("kotlin")))
    println(Json.encodeToString(data))
}

You can get the full code here.

You end up with a single JSON object, not an array with one element:

text
{"name":"kotlinx.serialization","users":{"name":"kotlin"}}

Manipulating default values

Another kind of useful transformation is omitting specific values from the output JSON, for example, if it
is used as default when missing or for other reasons.

Imagine that you cannot specify a default value for the language property in the Project data model for some reason,
but you need it omitted from the JSON when it is equal to Kotlin (we can all agree that Kotlin should be default anyway).
You can fix it by writing the special ProjectSerializer based on
the Plugin-generated serializer for the Project class.

kotlin
@Serializable
class Project(val name: String, val language: String)

object ProjectSerializer : JsonTransformingSerializer(Project.serializer()) {
    override fun transformSerialize(element: JsonElement): JsonElement =
        // Filter out top-level key value pair with the key "language" and the value "Kotlin"
        JsonObject(element.jsonObject.filterNot {
            (k, v) -> k == "language" && v.jsonPrimitive.content == "Kotlin"
        })
}

In the example below, we are serializing the Project class at the top-level, so we explicitly
pass the above ProjectSerializer to Json.encodeToString function as was shown in
the Passing a serializer manually section:

kotlin
fun main() {
    val data = Project("kotlinx.serialization", "Kotlin")
    println(Json.encodeToString(data)) // using plugin-generated serializer
    println(Json.encodeToString(ProjectSerializer, data)) // using custom serializer
}

You can get the full code here.

See the effect of the custom serializer:

text
{"name":"kotlinx.serialization","language":"Kotlin"}
{"name":"kotlinx.serialization"}

Content-based polymorphic deserialization

Typically, polymorphic serialization requires a dedicated "type" key
(also known as class discriminator) in the incoming JSON object to determine the actual serializer
which should be used to deserialize Kotlin class.

However, sometimes the type property may not be present in the input. In this case, you need to guess
the actual type by the shape of JSON, for example by the presence of a specific key.

JsonContentPolymorphicSerializer provides a skeleton implementation for such a strategy.
To use it, override its selectDeserializer method.
Let's start with the following class hierarchy.

Note that is does not have to be sealed as recommended in the Sealed classes section,
because we are not going to take advantage of the plugin-generated code that automatically selects the
appropriate subclass, but are going to implement this code manually.

kotlin
@Serializable
abstract class Project {
    abstract val name: String
}

@Serializable
data class BasicProject(override val name: String): Project()


@Serializable
data class OwnedProject(override val name: String, val owner: String) : Project()

You can distinguish the BasicProject and OwnedProject subclasses by the presence of
the owner key in the JSON object.

kotlin
object ProjectSerializer : JsonContentPolymorphicSerializer(Project::class) {
    override fun selectDeserializer(element: JsonElement) = when {
        "owner" in element.jsonObject -> OwnedProject.serializer()
        else -> BasicProject.serializer()
    }
}

When you use this serializer to serialize data, either registered or
the default serializer is selected for the actual type at runtime:

kotlin
fun main() {
    val data = listOf(
        OwnedProject("kotlinx.serialization", "kotlin"),
        BasicProject("example")
    )
    val string = Json.encodeToString(ListSerializer(ProjectSerializer), data)
    println(string)
    println(Json.decodeFromString(ListSerializer(ProjectSerializer), string))
}

You can get the full code here.

No class discriminator is added in the JSON output:

text
[{"name":"kotlinx.serialization","owner":"kotlin"},{"name":"example"}]
[OwnedProject(name=kotlinx.serialization, owner=kotlin), BasicProject(name=example)]

Extending the behavior of the plugin generated serializer

In some cases, it may be necessary to add additional serialization logic on top of the plugin generated logic.
For example, to add a preliminary modification of JSON elements or to add processing of unknown values of enums.

In this case, you can mark the serializable class with the @KeepGeneratedSerializer annotation and get the generated serializer using the generatedSerializer() function.

This annotation is currently experimental. Kotlin 2.0.20 or higher is required for this feature to work.

Here is an example of the simultaneous use of JsonTransformingSerializer and polymorphism.
In this example, we use transformDeserialize function to rename basic-name key into name so it matches the abstract val name property from the Project supertype.

kotlin
@Serializable
sealed class Project {
    abstract val name: String
}

@OptIn(ExperimentalSerializationApi::class)
@KeepGeneratedSerializer
@Serializable(with = BasicProjectSerializer::class)
@SerialName("basic")
data class BasicProject(override val name: String): Project()

object BasicProjectSerializer : JsonTransformingSerializer(BasicProject.generatedSerializer()) {
    override fun transformDeserialize(element: JsonElement): JsonElement {
        val jsonObject = element.jsonObject
        return if ("basic-name" in jsonObject) {
            val nameElement = jsonObject["basic-name"] ?: throw IllegalStateException()
            JsonObject(mapOf("name" to nameElement))
        } else {
            jsonObject
        }
    }
}


fun main() {
    val project = Json.decodeFromString("""{"type":"basic","basic-name":"example"}""")
    println(project)
}

You can get the full code here.

BasicProject will be printed to the output:

text
BasicProject(name=example)

Under the hood (experimental)

Although abstract serializers mentioned above can cover most of the cases, it is possible to implement similar machinery
manually, using only the KSerializer class.
If tweaking the abstract methods transformSerialize/transformDeserialize/selectDeserializer is not enough,
then altering serialize/deserialize is a way to go.

Here are some useful things about custom serializers with Json:

Given all that, it is possible to implement two-stage conversion Decoder -> JsonElement -> value or
value -> JsonElement -> Encoder.
For example, you can implement a fully custom serializer for the following Response class so that its
Ok subclass is represented directly, but the Error subclass is represented by an object with the error message:

kotlin
@Serializable(with = ResponseSerializer::class)
sealed class Response<out T> {
    data class Ok<out T>(val data: T) : Response<T>()
    data class Error(val message: String) : Response<Nothing>()
}

class ResponseSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Response<T>> {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Response") {
        element("Ok", dataSerializer.descriptor)
        element("Error", buildClassSerialDescriptor("Error") {
          element<String>("message")
        })
    }

    override fun deserialize(decoder: Decoder): Response<T> {
        // Decoder -> JsonDecoder
        require(decoder is JsonDecoder) // this class can be decoded only by Json
        // JsonDecoder -> JsonElement
        val element = decoder.decodeJsonElement()
        // JsonElement -> value
        if (element is JsonObject && "error" in element)
            return Response.Error(element["error"]!!.jsonPrimitive.content)
        return Response.Ok(decoder.json.decodeFromJsonElement(dataSerializer, element))
    }

    override fun serialize(encoder: Encoder, value: Response<T>) {
        // Encoder -> JsonEncoder
        require(encoder is JsonEncoder) // This class can be encoded only by Json
        // value -> JsonElement
        val element = when (value) {
            is Response.Ok -> encoder.json.encodeToJsonElement(dataSerializer, value.data)
            is Response.Error -> buildJsonObject { put("error", value.message) }
        }
        // JsonElement -> JsonEncoder
        encoder.encodeJsonElement(element)
    }
}

Having this serializable Response implementation, you can take any serializable payload for its data
and serialize or deserialize the corresponding responses:

kotlin
@Serializable
data class Project(val name: String)

fun main() {
    val responses = listOf(
        Response.Ok(Project("kotlinx.serialization")),
        Response.Error("Not found")
    )
    val string = Json.encodeToString(responses)
    println(string)
    println(Json.decodeFromString<List<Response>>(string))
}

You can get the full code here.

This gives you fine-grained control on the representation of the Response class in the JSON output:

text
[{"name":"kotlinx.serialization"},{"error":"Not found"}]
[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]

Maintaining custom JSON attributes

A good example of custom JSON-specific serializer would be a deserializer
that packs all unknown JSON properties into a dedicated field of JsonObject type.

Let's add UnknownProject – a class with the name property and arbitrary details flattened into the same object:

kotlin
data class UnknownProject(val name: String, val details: JsonObject)

However, the default plugin-generated serializer requires details
to be a separate JSON object and that's not what we want.

To mitigate that, write an own serializer that uses the fact that it works only with the Json format:

kotlin
object UnknownProjectSerializer : KSerializer {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") {
        element<String>("name")
        element<JsonElement>("details")
    }

    override fun deserialize(decoder: Decoder): UnknownProject {
        // Cast to JSON-specific interface
        val jsonInput = decoder as? JsonDecoder ?: error("Can be deserialized only by JSON")
        // Read the whole content as JSON
        val json = jsonInput.decodeJsonElement().jsonObject
        // Extract and remove name property
        val name = json.getValue("name").jsonPrimitive.content
        val details = json.toMutableMap()
        details.remove("name")
        return UnknownProject(name, JsonObject(details))
    }

    override fun serialize(encoder: Encoder, value: UnknownProject) {
        error("Serialization is not supported")
    }
}

Now it can be used to read flattened JSON details as UnknownProject:

kotlin
fun main() {
    println(Json.decodeFromString(UnknownProjectSerializer, """{"type":"unknown","name":"example","maintainer":"Unknown","license":"Apache 2.0"}"""))
}

You can get the full code here.

text
UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"})

The next chapter covers Alternative and custom formats (experimental).


File: docs/migration.md

Migration from 0.20.0 version to 1.0.0

For adopters of earlier versions of kotlinx.serialization, a dedicated migration path is prepared.
During the preparation of serialization 1.0.0 release, most of the API has been changed, renamed, moved to
a separate package or made internal. IDEA migrations were introduced, but unfortunately not all API can be migrated
with automatic replacements.

To simplify your migrations path, it is recommended to enable star imports in IDE (so all extensions are imported automatically) first.

  1. Update kotlinx.serialization to version 1.0.0-RC2 (this is the last version that has migrations for pre-1.0.0 versions. 1.0.0 version itself does not have any migration aids.)
  2. Rename dependency from kotlinx-serialization-runtime to kotlinx-serialization-json.
  3. For multiplatform usages, remove dependencies to platform-specific artifacts (e.g. kotlinx-serialization-runtime-js), they are no longer required by Gradle.
  4. Update Kotlin to 1.4.0 or higher.
  5. Start applying replacements for the deprecated code.
  6. If some signatures are not resolved, try to hit alt + Enter and import the signature.
  7. If methods are still not resolved, it is recommended to use star imports for kotlinx.serialization signatures in the problematic file.
  8. When there are no usages of deprecated code left, you can change dependency version from 1.0.0-RC2 to 1.0.0.

For less trivial issues, it is recommended to study the changelog or to ask for help in #serialization Kotlin's Slack channel.


File: docs/polymorphism.md

Polymorphism

This is the fourth chapter of the Kotlin Serialization Guide.
In this chapter we'll see how Kotlin Serialization deals with polymorphic class hierarchies.

Table of contents

Closed polymorphism

Let us start with basic introduction to polymorphism.

Static types

Kotlin Serialization is fully static with respect to types by default. The structure of encoded objects is determined
by compile-time types of objects. Let's examine this aspect in more detail and learn how
to serialize polymorphic data structures, where the type of data is determined at runtime.

To show the static nature of Kotlin Serialization let us make the following setup. An open class Project
has just the name property, while its derived class OwnedProject adds an owner property.
In the below example, we serialize data variable with a static type of
Project that is initialized with an instance of OwnedProject at runtime.

kotlin
@Serializable
open class Project(val name: String)

class OwnedProject(name: String, val owner: String) : Project(name)

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data))
}

You can get the full code here.

Despite the runtime type of OwnedProject, only the Project class properties are getting serialized.

text
{"name":"kotlinx.coroutines"}

Let's change the compile-time type of data to OwnedProject.

kotlin
@Serializable
open class Project(val name: String)

class OwnedProject(name: String, val owner: String) : Project(name)

fun main() {
    val data = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data))
}

You can get the full code here.

We get an error, because the OwnedProject class is not serializable.

text
Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'OwnedProject' is not found.
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.

Designing serializable hierarchy

We cannot simply mark OwnedProject from the previous example as @Serializable. It does not compile,
running into the constructor properties requirement.
To make hierarchy of classes serializable, the properties in the parent class have to be marked abstract,
making the Project class abstract, too.

kotlin
@Serializable
abstract class Project {
    abstract val name: String
}

class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data))
}

You can get the full code here.

This is close to the best design for a serializable hierarchy of classes, but running it produces the following error:

text
Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for subclass 'OwnedProject' is not found in the polymorphic scope of 'Project'.
Check if class with serial name 'OwnedProject' exists and serializer is registered in a corresponding SerializersModule.
To be registered automatically, class 'OwnedProject' has to be '@Serializable', and the base class 'Project' has to be sealed and '@Serializable'.

Sealed classes

The most straightforward way to use serialization with a polymorphic hierarchy is to mark the base class sealed.
All subclasses of a sealed class must be explicitly marked as @Serializable.

kotlin
@Serializable
sealed class Project {
    abstract val name: String
}
            
@Serializable
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data)) // Serializing data of compile-time type Project
}

You can get the full code here.

Now we can see a default way to represent polymorphism in JSON.
A type key is added to the resulting JSON object as a discriminator.

text
{"type":"example.examplePoly04.OwnedProject","name":"kotlinx.coroutines","owner":"kotlin"}

Pay attention to the small, but very important detail in the above example that is related to Static types:
the val data property has a compile-time type of Project, even though its run-time type is OwnedProject.
When serializing polymorphic class hierarchies you must ensure that the compile-time type of the serialized object
is a polymorphic one, not a concrete one.

Let us see what happens if the example is slightly changed, so that the compile-time of the object that is being
serialized is OwnedProject (the same as its run-time type).

kotlin
@Serializable
sealed class Project {
    abstract val name: String
}
            
@Serializable
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data = OwnedProject("kotlinx.coroutines", "kotlin") // data: OwnedProject here
    println(Json.encodeToString(data)) // Serializing data of compile-time type OwnedProject
}

You can get the full code here.

The type of OwnedProject is concrete and is not polymorphic, thus the type
discriminator property is not emitted into the resulting JSON.

text
{"name":"kotlinx.coroutines","owner":"kotlin"}

In general, Kotlin Serialization is designed to work correctly only when the compile-time type used during serialization
is the same one as the compile-time type used during deserialization. You can always specify the type explicitly
when calling serialization functions. The previous example can be corrected to use Project type for serialization
by calling Json.encodeToString(data).

Custom subclass serial name

A value of the type key is a fully qualified class name by default. We can put SerialName annotation onto
the corresponding class to change it.

kotlin
@Serializable
sealed class Project {
    abstract val name: String
}
            
@Serializable         
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data))
}

You can get the full code here.

This way we can have a stable serial name that is not affected by the class's name in the source code.

text
{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}

In addition to that, JSON can be configured to use a different key name for the class discriminator.
You can find an example in the Class discriminator for polymorphism section.

[!IMPORTANT]
When picking a serial name for a class, avoid assigning the same name to different classes.
Check out equality rules in documentation for SerialDescriptor to make sure that the class descriptor will stay unique.

Concrete properties in a base class

A base class in a sealed hierarchy can have properties with backing fields.

kotlin
@Serializable
sealed class Project {
    abstract val name: String   
    var status = "open"
}
            
@Serializable   
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val json = Json { encodeDefaults = true } // "status" will be skipped otherwise
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(json.encodeToString(data))
}

You can get the full code here.

The properties of the superclass are serialized before the properties of the subclass.

text
{"type":"owned","status":"open","name":"kotlinx.coroutines","owner":"kotlin"}

Objects

Sealed hierarchies can have objects as their subclasses and they also need to be marked as @Serializable.
Let's take a different example with a hierarchy of Response classes.

kotlin
@Serializable
sealed class Response
                      
@Serializable
object EmptyResponse : Response()

@Serializable   
class TextResponse(val text: String) : Response()

Let us serialize a list of different responses.

kotlin
fun main() {
    val list = listOf(EmptyResponse, TextResponse("OK"))
    println(Json.encodeToString(list))
}

You can get the full code here.

An object serializes as an empty class, also using its fully qualified class name as type by default:

text
[{"type":"example.examplePoly08.EmptyResponse"},{"type":"example.examplePoly08.TextResponse","text":"OK"}]

Even if object has properties, they are not serialized.

Open polymorphism

Serialization can work with arbitrary open classes or abstract classes.
However, since this kind of polymorphism is open, there is a possibility that subclasses are defined anywhere in the
source code, even in other modules, the list of subclasses that are serialized cannot be determined at compile-time and
must be explicitly registered at runtime.

Registered subclasses

Let us start with the code from the Designing serializable hierarchy section.
To make it work with serialization without making it sealed, we have to define a SerializersModule using the
SerializersModule {} builder function. In the module the base class is specified
in the polymorphic builder and each subclass is registered with the subclass function. Now,
a custom JSON configuration can be instantiated with this module and used for serialization.

Details on custom JSON configurations can be found in
the JSON configuration section.

kotlin
val module = SerializersModule {
    polymorphic(Project::class) {
        subclass(OwnedProject::class)
    }
}

val format = Json { serializersModule = module }

@Serializable
abstract class Project {
    abstract val name: String
}
            
@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(data))
}

You can get the full code here.

This additional configuration makes our code work just as it worked with a sealed class in
the Sealed classes section, but here subclasses can be spread arbitrarily throughout the code.

text
{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}

Please note that this example works only on JVM because of serializer function restrictions.
For JS and Native, explicit serializer should be used: format.encodeToString(PolymorphicSerializer(Project::class), data)
You can keep track of this issue here.

Serializing interfaces

We can update the previous example and turn Project superclass into an interface. However, we cannot
mark an interface itself as @Serializable. No problem. Interfaces cannot have instances by themselves.
Interfaces can only be represented by instances of their derived classes. Interfaces are used in the Kotlin language to enable polymorphism,
so all interfaces are considered to be implicitly serializable with the PolymorphicSerializer
strategy. We just need to mark their implementing classes as @Serializable and register them.

kotlin
interface Project {
    val name: String
}

@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project

Now if we declare data with the type of Project we can simply call format.encodeToString as before.

kotlin
fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(data))
}

You can get the full code here.

text
{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}

Note: On Kotlin/Native, you should use format.encodeToString(PolymorphicSerializer(Project::class), data)) instead due to limited reflection capabilities.

Registering sealed children as subclasses

A sealed parent interface or class can be used to directly register all its children using subclassesOfSealed.
This will allow serializing the children using open polymorphism without the need to register each one individually.

If one of the type's subclasses is a sealed serializable class on its own, its subclasses are registered recursively
as well. However, if one of the type's subclasses is an open polymorphic class, an IllegalArgumentException is thrown.
In other words, all children/descendants must be either concrete or sealed.

kotlin
interface Base

@Serializable
sealed interface Sub: Base

@Serializable
class Sub1(val data: String): Sub

val module1 = SerializersModule {
  polymorphic(Base::class) {
     subclassesOfSealed(Sub.serializer())
  }
}

val format1 = Json { serializersModule = module1 }

Alternatively the convenience overload allows specifying the sealed type as type parameter.

kotlin
val module2 = SerializersModule {
  polymorphic(Base::class) {
     subclassesOfSealed<Sub>()
  }
}

val format2 = Json { serializersModule = module2 }

Now if we declare data with the type of Base we can simply call format.encodeToString as before.

kotlin
fun main() {
    val data: Base = Sub1("kotlin")
    println(format1.encodeToString(data))
    println(format2.encodeToString(data))
}
text
{"type":"example.examplePoly11.Sub1","data":"kotlin"}
{"type":"example.examplePoly11.Sub1","data":"kotlin"}

You can get the full code here.

Property of an interface type

Continuing the previous example, let us see what happens if we use Project interface as a property in some
other serializable class. Interfaces are implicitly polymorphic, so we can just declare a property of an interface type.

kotlin
@Serializable
class Data(val project: Project) // Project is an interface

fun main() {
    val data = Data(OwnedProject("kotlinx.coroutines", "kotlin"))
    println(format.encodeToString(data))
}

You can get the full code here.

As long as we've registered the actual subtype of the interface that is being serialized in
the SerializersModule of our format, we get it working at runtime.

text
{"project":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}}

Static parent type lookup for polymorphism

During serialization of a polymorphic class the root type of the polymorphic hierarchy (Project in our example)
is determined statically. Let us take the example with the serializable abstract class Project,
but change the main function to declare data as having a type of Any:

kotlin
fun main() {
    val data: Any = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(data))
}

You can get the full code here.

We get the exception.

text
Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found.
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.

We have to register classes for polymorphic serialization with respect for the corresponding static type we
use in the source code. First of all, we change our module to register a subclass of Any:

kotlin
val module = SerializersModule {
    polymorphic(Any::class) {
        subclass(OwnedProject::class)
    }
}

Then we can try to serialize the variable of type Any:

kotlin
fun main() {
    val data: Any = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(data))
}

You can get the full code here.

However, Any is a class and it is not serializable:

text
Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found.
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.

We must to explicitly pass an instance of PolymorphicSerializer for the base class Any as the
first parameter to the encodeToString function.

kotlin
fun main() {
    val data: Any = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(PolymorphicSerializer(Any::class), data))
}

You can get the full code here.

With the explicit serializer it works as before.

text
{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}

Explicitly marking polymorphic class properties

The property of an interface type is implicitly considered polymorphic, since interfaces are all about runtime polymorphism.
However, Kotlin Serialization does not compile a serializable class with a property of a non-serializable class type.
If we have a property of Any class or other non-serializable class, then we must explicitly provide its serialization
strategy via the @Serializable annotation as we saw in
the Specifying serializer on a property section.
To specify a polymorphic serialization strategy of a property, the special-purpose @Polymorphic
annotation is used.

kotlin
@Serializable
class Data(
    @Polymorphic // the code does not compile without it 
    val project: Any 
)

fun main() {
    val data = Data(OwnedProject("kotlinx.coroutines", "kotlin"))
    println(format.encodeToString(data))
}

You can get the full code here.

Registering multiple superclasses

When the same class gets serialized as a value of properties with different compile-time type from the list of
its superclasses, we must register it in the SerializersModule for each of its superclasses separately.
It is convenient to extract registration of all the subclasses into a separate function and
use it for each superclass. You can use the following template to write it.

kotlin
val module = SerializersModule {
    fun PolymorphicModuleBuilder.registerProjectSubclasses() {
        subclass(OwnedProject::class)
    }
    polymorphic(Any::class) { registerProjectSubclasses() }
    polymorphic(Project::class) { registerProjectSubclasses() }
}

You can get the full code here.

Polymorphism and generic classes

Generic subtypes for a serializable class require a special handling. Consider the following hierarchy.

kotlin
@Serializable
abstract class Response<out T>
            
@Serializable
@SerialName("OkResponse")
data class OkResponse<out T>(val data: T) : Response<T>()

Kotlin Serialization does not have a builtin strategy to represent the actually provided argument type for the
type parameter T when serializing a property of the polymorphic type OkResponse<T>. We have to provide this
strategy explicitly when defining the serializers module for Response. In the below example we
use OkResponse.serializer(...) to retrieve
the Plugin-generated generic serializer of
the OkResponse class and instantiate it with the PolymorphicSerializer instance with
Any class as its base. This way, we can serialize an instance of OkResponse with any data property that
was polymorphically registered as a subtype of Any.

kotlin
val responseModule = SerializersModule {
    polymorphic(Response::class) {
        subclass(OkResponse.serializer(PolymorphicSerializer(Any::class)))
    }
}

Merging library serializers modules

When the application grows in size and splits into source code modules,
it may become inconvenient to store all class hierarchies in one serializers module.
Let us add a library with the Project hierarchy to the code from the previous section.

kotlin
val projectModule = SerializersModule {
    fun PolymorphicModuleBuilder.registerProjectSubclasses() {
        subclass(OwnedProject::class)
    }
    polymorphic(Any::class) { registerProjectSubclasses() }
    polymorphic(Project::class) { registerProjectSubclasses() }
}

We can compose those two modules together using the plus operator to merge them,
so that we can use them both in the same Json format instance.

You can also use the include function
in the SerializersModule {} DSL.

kotlin
val format = Json { serializersModule = projectModule + responseModule }

Now classes from both hierarchies can be serialized together and deserialized together.

kotlin
fun main() {
    // both Response and Project are abstract and their concrete subtypes are being serialized
    val data: Response =  OkResponse(OwnedProject("kotlinx.serialization", "kotlin"))
    val string = format.encodeToString(data)
    println(string)
    println(format.decodeFromString<Response>(string))
}

You can get the full code here.

The JSON that is being produced is deeply polymorphic.

text
{"type":"OkResponse","data":{"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"}}
OkResponse(data=OwnedProject(name=kotlinx.serialization, owner=kotlin))

If you're writing a library or shared module with an abstract class and some implementations of it,
you can expose your own serializers module for your clients to use so that a client can combine your
module with their modules.

Default polymorphic type handler for deserialization

What happens when we deserialize a subclass that was not registered?

kotlin
fun main() {
    println(format.decodeFromString("""
        {"type":"unknown","name":"example"}
    """))
}

You can get the full code here.

We get the following exception.

text
Exception in thread "main" kotlinx.serialization.json.JsonDecodingException: Unexpected JSON token at offset 0: Serializer for subclass 'unknown' is not found in the polymorphic scope of 'Project' at path: $
Check if class with serial name 'unknown' exists and serializer is registered in a corresponding SerializersModule.

When reading a flexible input we might want to provide some default behavior in this case. For example,
we can have a BasicProject subtype to represent all kinds of unknown Project subtypes.

kotlin
@Serializable
abstract class Project {
    abstract val name: String
}

@Serializable
data class BasicProject(override val name: String, val type: String): Project()

@Serializable
@SerialName("OwnedProject")
data class OwnedProject(override val name: String, val owner: String) : Project()

We register a default deserializer handler using the defaultDeserializer function in
the polymorphic { ... } DSL that defines a strategy which maps the type string from the input
to the deserialization strategy. In the below example we don't use the type,
but always return the Plugin-generated serializer
of the BasicProject class.

kotlin
val module = SerializersModule {
    polymorphic(Project::class) {
        subclass(OwnedProject::class)
        defaultDeserializer { BasicProject.serializer() }
    }
}

Using this module we can now deserialize both instances of the registered OwnedProject and
any unregistered one.

kotlin
val format = Json { serializersModule = module }

fun main() {
    println(format.decodeFromString<List>("""
        [
            {"type":"unknown","name":"example"},
            {"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"} 
        ]
    """))
}

You can get the full code here.

Notice, how BasicProject had also captured the specified type key in its type property.

text
[BasicProject(name=example, type=unknown), OwnedProject(name=kotlinx.serialization, owner=kotlin)]

We used a plugin-generated serializer as a default serializer, implying that
the structure of the "unknown" data is known in advance. In a real-world API it's rarely the case.
For that purpose a custom, less-structured serializer is needed. You will see the example of such serializer in the future section
on Maintaining custom JSON attributes.

Default polymorphic type handler for serialization

Sometimes you need to dynamically choose which serializer to use for a polymorphic type based on the instance, for example if you
don't have access to the full type hierarchy, or if it changes a lot. For this situation, you can register a default serializer.

kotlin
interface Animal {
}

interface Cat : Animal {
    val catType: String
}

interface Dog : Animal {
    val dogType: String
}

private class CatImpl : Cat {
    override val catType: String = "Tabby"
}

private class DogImpl : Dog {
    override val dogType: String = "Husky"
}

object AnimalProvider {
    fun createCat(): Cat = CatImpl()
    fun createDog(): Dog = DogImpl()
}

We register a default serializer handler using the polymorphicDefaultSerializer function in
the SerializersModule { ... } DSL that defines a strategy which takes an instance of the base class and
provides a serialization strategy. In the below example we use a when block to check the type of the
instance, without ever having to refer to the private implementation classes.

kotlin
val module = SerializersModule {
    polymorphicDefaultSerializer(Animal::class) { instance ->
        @Suppress("UNCHECKED_CAST")
        when (instance) {
            is Cat -> CatSerializer as SerializationStrategy<Animal>
            is Dog -> DogSerializer as SerializationStrategy<Animal>
            else -> null
        }
    }
}

object CatSerializer : SerializationStrategy<Cat> {
    override val descriptor = buildClassSerialDescriptor("Cat") {
        element<String>("catType")
    }
  
    override fun serialize(encoder: Encoder, value: Cat) {
        encoder.encodeStructure(descriptor) {
          encodeStringElement(descriptor, 0, value.catType)
        }
    }
}

object DogSerializer : SerializationStrategy<Dog> {
  override val descriptor = buildClassSerialDescriptor("Dog") {
    element<String>("dogType")
  }

  override fun serialize(encoder: Encoder, value: Dog) {
    encoder.encodeStructure(descriptor) {
      encodeStringElement(descriptor, 0, value.dogType)
    }
  }
}

Using this module we can now serialize instances of Cat and Dog.

kotlin
val format = Json { serializersModule = module }

fun main() {
    println(format.encodeToString<Animal>(AnimalProvider.createCat()))
}

You can get the full code here

text
{"type":"Cat","catType":"Tabby"}

The next chapter covers JSON features.

2. Official Technical Reference & Guides (Kotlin/Kotlin.github.io)

Kotlin GitHub Page

This repo exists solely to populate kotlin.github.io and redirects to kotlinlang.org.

It also provides titles and favicons for kotlin.github.io/* websites in search consoles like Google.

Code of Conduct

The JetBrains Code of Conduct can be found here and this project also adheres to it.

Contribution

We won't accept pull requests unless there's broken/outdated behavior regarding either:

  • the favicon (for instance, it's out of date)
  • the title
  • the redirect to kotlinlang.org
  • a missing html tag that prevents other search consoles from picking up the relevant icon or title(s).

Why does this exist?

This repo was created to fix the issue KTL-1336.