Kotlin Help

Create and use serializers

A serializer defines the structure of a Kotlin type in its serialized form, while format implementations such as Json control how that structure is encoded.

Diagram where a Kotlin value is serialized by a serializer into a sequence of primitives, encoded by a format into encoded data, decoded back into a sequence of primitives, and deserialized by a serializer into a Kotlin value

Serializers define serialization and deserialization strategies for a type through the KSerializer interface. Kotlin serialization provides serializers for built-in types, collections, and more.

You can use these serializers to serialize values or inspect the structure of their serialized form, while custom serializers let you define that structure yourself.

Obtain serializers

When you annotate a class with @Serializable, the Kotlin serialization plugin generates a KSerializer for that class.

To retrieve this automatically generated serializer, call the generated .serializer() function. You can use a serializer directly with functions such as Json.encodeToString(), or access its descriptor property to inspect the structure of the type's serialized form.

Here's an example that defines a Color class with a single integer property and inspects the structure of its serialized form:

import kotlinx.serialization.* //sampleStart @Serializable data class Color(val rgb: Int) fun main() { // Retrieves the generated serializer for the Color class val colorSerializer: KSerializer<Color> = Color.serializer() println(colorSerializer.descriptor) // Color(rgb: kotlin.Int) } //sampleEnd

You can use the top-level serializer<T>() function to obtain a serializer for any type, including parameterized ones:

import kotlinx.serialization.* //sampleStart @Serializable @SerialName("Color") class Color(val rgb: Int) fun main() { // Retrieves the serializer for the Map<String, Color> val stringToColorMapSerializer: KSerializer<Map<String, Color>> = serializer() // Prints: kotlin.collections.LinkedHashMap(PrimitiveDescriptor(kotlin.String), Color(rgb: kotlin.Int)) println(stringToColorMapSerializer.descriptor) } //sampleEnd

For generic classes, if you want to use the generated .serializer() function, provide one KSerializer argument for each type parameter:

import kotlinx.serialization.* //sampleStart @Serializable @SerialName("Color") class Color(val rgb: Int) @Serializable @SerialName("Box") class Box<T>(val contents: T) fun main() { // Calls .serializer() using a KSerializer for the type parameter val boxedColorSerializer = Box.serializer(Color.serializer()) println(boxedColorSerializer.descriptor) // Box(contents: Color) } //sampleEnd

Obtain serializers for collection types

Unlike classes annotated with @Serializable, collection types like List<T> don't have a generated .serializer() function.

To obtain a serializer for a collection type, create one with ListSerializer(), SetSerializer(), or MapSerializer() and specify serializers for the collection's type parameters.

Here's an example that uses the ListSerializer() function to create a serializer for List<String>:

import kotlinx.serialization.* import kotlinx.serialization.builtins.* //sampleStart fun main() { val stringListSerializer: KSerializer<List<String>> = ListSerializer(String.serializer()) println(stringListSerializer.descriptor) // kotlin.collections.ArrayList(PrimitiveDescriptor(kotlin.String)) } //sampleEnd

Create custom serializers

If you want more control over the structure of your serialized data, you can create a custom serializer. A custom serializer lets you define how a type is represented in its serialized form.

Like generated serializers, custom serializers define both serialization and deserialization for a type through the KSerializer interface. To support both, KSerializer extends SerializationStrategy and DeserializationStrategy.

Create a custom primitive serializer

Use a primitive serializer to represent a class as a single primitive value, such as a string or integer.

To create a custom primitive serializer:

  1. Create a custom serializer as an object that implements KSerializer for the serialized class:

    object YourSerializer : KSerializer<Type>
  2. Override the descriptor property to define the schema for the serialized data.

    Use the PrimitiveSerialDescriptor(serialName, kind) to define the structure of the serialized form as a single primitive value. Specify a unique serialName, such as a fully qualified name, and use a PrimitiveKind that matches the encoder and decoder functions used in the serializer:

    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("com.example.Type", PrimitiveKind.STRING)

  3. Implement the serialize() function to define how values are converted to their serialized form. Choose an Encoder function that matches the PrimitiveKind in the descriptor:

    override fun serialize(encoder: Encoder, value: Type) { val encodedValue: String = // convert value to a primitive representation encoder.encodeString(encodedValue) }
  4. Implement the deserialize() function to define how to convert the serialized data back into an instance of your class. The Decoder provides functions to read the data, such as decodeString().

    override fun deserialize(decoder: Decoder): Type { val decodedValue: String = decoder.decodeString() // Converts decodedValue back to Type return ... }
  5. Use the @Serializable annotation to specify a custom serializer for the class:

    @Serializable(YourSerializer::class) data class Type(val stringValue: String)

Here's an example for a custom primitive serializer that serializes Color as a hexadecimal string:

import kotlinx.serialization.* import kotlinx.serialization.encoding.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.json.* //sampleStart // Creates the custom serializer for the Color class object ColorAsStringSerializer : KSerializer<Color> { // Defines the schema for the serialized data as a single string override val descriptor: SerialDescriptor = // Specifies a unique name and a PrimitiveKind PrimitiveSerialDescriptor("my.app.Color", PrimitiveKind.STRING) // Defines how a Color value is serialized as a string override fun serialize(encoder: Encoder, value: Color) { // Converts the RGB value to a hexadecimal string val hexValue = value.rgb.toString(16).padStart(6, '0') // Encodes the serialized value as a string encoder.encodeString(hexValue) } // Defines how a Color value is deserialized from a string override fun deserialize(decoder: Decoder): Color { // Decodes the serialized string value val hexValue = decoder.decodeString() // Converts the decoded value back into a Color return Color(hexValue.toInt(16)) } } // Specifies ColorAsStringSerializer as the custom serializer for the Color class @Serializable(ColorAsStringSerializer::class) data class Color(val rgb: Int) fun main() { val color = Color(0x00FF00) // Serializes a Color value to JSON val jsonString = Json.encodeToString(color) println(jsonString) // "00ff00" // Deserializes the JSON string into a Color value val deserializedColor = Json.decodeFromString<Color>(jsonString) println(deserializedColor.rgb) // 65280 } //sampleEnd

Serialize binary data as Base64 strings

You can use a custom primitive serializer to solve common serialization tasks beyond simple value conversion. One such task is representing binary data as a Base64 string.

Different APIs use different Base64 variants by default. Choose a Kotlin Base64 encoder that matches the expected variant, such as Base64.Default or Base64.Mime.

Here's an example that serializes a ByteArray as a Base64 string with Base64.Default in JSON format:

import kotlinx.serialization.* import kotlinx.serialization.json.* import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.descriptors.* import kotlin.io.encoding.* //sampleStart // Creates a custom primitive serializer, // which represents ByteArray as a Base64 string object ByteArrayAsBase64Serializer : KSerializer<ByteArray> { // Uses the default Base64 variant private val base64 = Base64.Default // Defines the serialized form as a single STRING primitive override val descriptor: SerialDescriptor get() = PrimitiveSerialDescriptor( "ByteArrayAsBase64Serializer", PrimitiveKind.STRING ) // Encodes the ByteArray as a Base64 string override fun serialize(encoder: Encoder, value: ByteArray) { val base64Encoded = base64.encode(value) encoder.encodeString(base64Encoded) } // Decodes the Base64 string back into a ByteArray override fun deserialize(decoder: Decoder): ByteArray { val base64Decoded = decoder.decodeString() return base64.decode(base64Decoded) } } @Serializable data class Value( // Specifies the custom serializer for this property @Serializable(ByteArrayAsBase64Serializer::class) val base64Input: ByteArray ) { // Implements value-based equality for 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) } // Computes hashCode based on array contents override fun hashCode(): Int { return base64Input.contentHashCode() } } fun main() { val string = "PNG_IMAGE_DATA" val value = Value(string.toByteArray()) // Serializes Value to JSON val encoded = Json.encodeToString(value) println(encoded) // {"base64Input":"UE5HX0lNQUdFX0RBVEE="} // Deserializes JSON back into Value val decoded = Json.decodeFromString<Value>(encoded) println(decoded.base64Input.decodeToString()) // PNG_IMAGE_DATA } //sampleEnd

Delegate serialization to another serializer

You can serialize a class as another type by delegating the serialization logic to the serializer for that type. For example, you can use this approach to serialize a class as a non-primitive type, such as an IntArray.

To delegate serialization, create a custom serializer that defines a property for the serializer of the delegated type:

  1. Override the descriptor property to wrap the delegated serializer's descriptor.

  2. Override the serialize() function to convert an instance of your class to the delegated type and use encoder.encodeSerializableValue() function to encode it with the delegated serializer.

  3. Override the deserialize() function to use the decoder.decodeSerializableValue() function with the delegated serializer to decode the value and convert it back into an instance of your class.

Here's an example that serializes a Color class as an IntArray by delegating the serialization logic to IntArraySerializer:

import kotlinx.serialization.* import kotlinx.serialization.encoding.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.builtins.IntArraySerializer import kotlinx.serialization.json.* //sampleStart // Creates a custom serializer that delegates to IntArraySerializer class ColorIntArraySerializer : KSerializer<Color> { private val delegateSerializer = IntArraySerializer() override val descriptor = SerialDescriptor("my.app.Color", delegateSerializer.descriptor) // Delegates serialization logic to IntArraySerializer override fun serialize(encoder: Encoder, value: Color) { val data = intArrayOf( (value.rgb shr 16) and 0xFF, (value.rgb shr 8) and 0xFF, value.rgb and 0xFF ) encoder.encodeSerializableValue(delegateSerializer, data) } // Delegates deserialization and converts IntArray back to Color override fun deserialize(decoder: Decoder): Color { val array = decoder.decodeSerializableValue(delegateSerializer) return Color((array[0] shl 16) or (array[1] shl 8) or array[2]) } } @Serializable(ColorIntArraySerializer::class) class Color(val rgb: Int) fun main() { val green = Color(0x00ff00) println(Json.encodeToString(green)) // [0,255,0] } //sampleEnd

While using the array representation isn't conventional in JSON, it can reduce the size of serialized data when used with a ByteArray and a binary format.

Serialize classes with a surrogate class

You can use a surrogate class, a class that matches the serialized form of another class, when you want to:

  • Change how a class is serialized without modifying the class itself.

  • Avoid creating a composite serializer.

  • Validate the serialized form before creating the original class.

  • Handle cases where direct serialization doesn't fit the class's rules.

You can make surrogate classes private, and use an init block to enforce constraints on the serial representation of the class. You can also define a custom serial name to keep the serialized type name unchanged.

Let's look at an example that serializes Color as a JSON object with specific ranges for the r, g, and b properties:

// Defines a surrogate that matches the serialized form @Serializable @SerialName("Color") private class ColorSurrogate(val r: Int, val g: Int, val b: Int) { init { // Enforces constraints on the serialized form require(r in 0..255 && g in 0..255 && b in 0..255) } }

Similarly to delegating serialization to another serializer, the custom serializer converts the original class to another representation and wraps that representation's automatically generated SerialDescriptor.

To retrieve the generated serializer for the surrogate class, use ColorSurrogate.serializer():

object ColorSerializer : KSerializer<Color> { // The serialNames of descriptors must be unique override val descriptor: SerialDescriptor = SerialDescriptor("my.app.Color", ColorSurrogate.serializer().descriptor) // Converts the original class to the surrogate representation override fun serialize(encoder: Encoder, value: Color) { val surrogate = ColorSurrogate((value.rgb shr 16) and 0xff, (value.rgb shr 8) and 0xff, value.rgb and 0xff) encoder.encodeSerializableValue(ColorSurrogate.serializer(), surrogate) } // Converts the surrogate representation back to the original class override fun deserialize(decoder: Decoder): Color { val surrogate = decoder.decodeSerializableValue(ColorSurrogate.serializer()) return Color((surrogate.r shl 16) or (surrogate.g shl 8) or surrogate.b) } }

Finally, specify the custom serializer for the class:

import kotlinx.serialization.* import kotlinx.serialization.encoding.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.builtins.IntArraySerializer import kotlinx.serialization.json.* // Defines a private surrogate class with custom properties @Serializable @SerialName("Color") private class ColorSurrogate(val r: Int, val g: Int, val b: Int) { init { // Enforces constraints on the serialized form require(r in 0..255 && g in 0..255 && b in 0..255) } } // Creates a custom serializer that converts to and from the surrogate object ColorSerializer : KSerializer<Color> { // Defines a unique serialName for the wrapped SerialDescriptor override val descriptor: SerialDescriptor = SerialDescriptor("my.app.Color", ColorSurrogate.serializer().descriptor) // Converts the original class to the surrogate representation override fun serialize(encoder: Encoder, value: Color) { val surrogate = ColorSurrogate((value.rgb shr 16) and 0xff, (value.rgb shr 8) and 0xff, value.rgb and 0xff) encoder.encodeSerializableValue(ColorSurrogate.serializer(), surrogate) } // Converts the surrogate representation back to the original class override fun deserialize(decoder: Decoder): Color { val surrogate = decoder.decodeSerializableValue(ColorSurrogate.serializer()) return Color((surrogate.r shl 16) or (surrogate.g shl 8) or surrogate.b) } } //sampleStart // Specifies ColorSerializer as the custom serializer for the class @Serializable(ColorSerializer::class) class Color(val rgb: Int) fun main() { val green = Color(0x00ff00) println(Json.encodeToString(green)) // {"r":0,"g":255,"b":0} } //sampleEnd

Create a custom composite serializer

You can use composite serializers to represent complex data structures, such as classes with multiple properties.

Compared to using a surrogate class, a custom composite serializer lets you define the serialized structure of the original class directly without an additional conversion step. This can also improve performance in some cases, but requires writing more of the serialization logic manually.

To create a custom composite serializer:

  1. Create a serializer as an object that implements KSerializer for the target class:

    object YourSerializer : KSerializer<Type>
  2. Override the descriptor property and define the serialization schema with the buildClassSerialDescriptor() function:

    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("com.example.Type") { // ... }
  3. Specify each property in the buildClassSerialDescriptor() function with the element() function. The order of elements determines their indices, starting from 0.

    The SerialKind of the serializer's descriptor determines what each element() represents. In a class descriptor, an element() represents a property, while in an enum descriptor, an element() represents constants such as RED or GREEN:

    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("com.example.Type") { element<Int>("first") element<Int>("second") }
  4. Implement the serialize() function using the encodeStructure() DSL. Inside its block, the lambda receiver is a CompositeEncoder, which you use to call functions such as encodeIntElement() for each field, following the order defined in the descriptor:

    override fun serialize(encoder: Encoder, value: Type) = encoder.encodeStructure(descriptor) { encodeIntElement(descriptor, 0, value.first) encodeIntElement(descriptor, 1, value.second) }
  5. Implement the deserialize() function using the decodeStructure() DSL. Inside its block, the lambda receiver is a CompositeDecoder, which you use to call functions like decodeIntElement() to decode each property.

    Most formats allow data to be encoded in an arbitrary order, which can differ from the order of elements in the serializer's descriptor. Use the decodeElementIndex() function to identify which element() to decode. It returns CompositeDecoder.DECODE_DONE when no more elements are left, which you use to stop decoding the current structure:

    override fun deserialize(decoder: Decoder): Type = decoder.decodeStructure(descriptor) { var first = 0 var second = "" // Uses decodeElementIndex to ensure correct decoding regardless of order while (true) { when (val index = decodeElementIndex(descriptor)) { 0 -> first = decodeStringElement(descriptor, 0) 1 -> second = decodeIntElement(descriptor, 1) CompositeDecoder.DECODE_DONE -> break else -> error("Unexpected index: $index") } } Type(first, second) }
  6. Use the @Serializable(YourSerializer::class) annotation to specify the custom serializer for the class.

Let's look at an example of how to serialize a Color class with multiple properties:

import kotlinx.serialization.* import kotlinx.serialization.encoding.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.json.* //sampleStart // Creates a custom serializer for the Color class with multiple properties object ColorAsObjectSerializer : KSerializer<Color> { // Defines the schema for the Color class override val descriptor: SerialDescriptor = buildClassSerialDescriptor("my.app.Color") { // Specifies each property with its type and name with the element() function element<Int>("r") element<Int>("g") element<Int>("b") } // Serializes the Color in the order specified in the descriptor override fun serialize(encoder: Encoder, value: Color) = encoder.encodeStructure(descriptor) { encodeIntElement(descriptor, 0, (value.rgb shr 16) and 0xff) encodeIntElement(descriptor, 1, (value.rgb shr 8) and 0xff) encodeIntElement(descriptor, 2, value.rgb and 0xff) } // Deserializes the data back into a Color object override fun deserialize(decoder: Decoder): Color = decoder.decodeStructure(descriptor) { // Temporary variables to hold the decoded values var r = -1 var g = -1 var b = -1 // Uses decodeElementIndex() since element order may vary by format while (true) { when (val index = decodeElementIndex(descriptor)) { 0 -> r = decodeIntElement(descriptor, 0) 1 -> g = decodeIntElement(descriptor, 1) 2 -> b = decodeIntElement(descriptor, 2) CompositeDecoder.DECODE_DONE -> break else -> error("Unexpected index: $index") } } // Validates values and reconstructs Color require(r in 0..255 && g in 0..255 && b in 0..255) Color((r shl 16) or (g shl 8) or b) } } // Specifies the custom serializer for Color @Serializable(ColorAsObjectSerializer::class) data class Color(val rgb: Int) fun main() { val color = Color(0x00ff00) val string = Json.encodeToString(color) println(string) // {"r":0,"g":255,"b":0} require(Json.decodeFromString<Color>(string) == color) } //sampleEnd

Encode default values in custom serializers

Plugin-generated serializers check whether the encoder needs to encode values that are equal to their defaults. For example, in JSON, this is controlled by the encodeDefaults property.

To achieve the same behavior in a custom serializer, use the shouldEncodeElementDefault() function with the serializer's descriptor and the index of the element you're encoding.

Here's an example where a custom serializer encodes default Color values when encodeDefaults is enabled:

import kotlinx.serialization.* import kotlinx.serialization.encoding.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.json.* //sampleStart // Creates a custom serializer for the Color class with multiple properties object ColorAsObjectSerializer : KSerializer<Color> { // Defines the schema for the Color class override val descriptor: SerialDescriptor = buildClassSerialDescriptor("my.app.Color") { // Specifies each property with its type and name with the element() function element<Int>("r", isOptional = true) element<Int>("g", isOptional = true) element<Int>("b", isOptional = true) } override fun serialize(encoder: Encoder, value: Color) = encoder.encodeStructure(descriptor) { val r = (value.rgb shr 16) and 0xff val g = (value.rgb shr 8) and 0xff val b = value.rgb and 0xff // Encodes r if it differs from its default value, // or if the encoder needs to encode the first element's default value if (r != 0 || shouldEncodeElementDefault(descriptor, 0)) { encodeIntElement(descriptor, 0, r) } if (g != 255 || shouldEncodeElementDefault(descriptor, 1)) { encodeIntElement(descriptor, 1, g) } if (b != 0 || shouldEncodeElementDefault(descriptor, 2)) { encodeIntElement(descriptor, 2, b) } } // Deserializes the data back into a Color object override fun deserialize(decoder: Decoder): Color = decoder.decodeStructure(descriptor) { var r = 0 var g = 255 var b = 0 while (true) { when (val index = decodeElementIndex(descriptor)) { 0 -> r = decodeIntElement(descriptor, 0) 1 -> g = decodeIntElement(descriptor, 1) 2 -> b = decodeIntElement(descriptor, 2) CompositeDecoder.DECODE_DONE -> break else -> error("Unexpected index: $index") } } require(r in 0..255 && g in 0..255 && b in 0..255) Color((r shl 16) or (g shl 8) or b) } } // Specifies the custom serializer for Color @Serializable(ColorAsObjectSerializer::class) data class Color(val rgb: Int = 0x00ff00) fun main() { val color = Color() val stringWithDefaults = Json { encodeDefaults = true }.encodeToString(color) println(stringWithDefaults) // {"r":0,"g":255,"b":0} } //sampleEnd

Optimize deserialization with sequential decoding

Some formats use a strictly ordered schema and can support sequential decoding. For these formats, you can optimize deserialization with the decodeSequentially() function. This function returns true if the current structure can be decoded in order. Handle that case separately to skip the more complex logic of decoding individual elements out of order.

Here's an example that uses decodeSequentially() to optimize deserialization when possible:

import kotlinx.serialization.* import kotlinx.serialization.encoding.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.json.* object ColorAsObjectSerializer : KSerializer<Color> { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("my.app.Color") { element<Int>("r") element<Int>("g") element<Int>("b") } override fun serialize(encoder: Encoder, value: Color) = encoder.encodeStructure(descriptor) { encodeIntElement(descriptor, 0, (value.rgb shr 16) and 0xff) encodeIntElement(descriptor, 1, (value.rgb shr 8) and 0xff) encodeIntElement(descriptor, 2, value.rgb and 0xff) } //sampleStart override fun deserialize(decoder: Decoder): Color = decoder.decodeStructure(descriptor) { var r = -1 var g = -1 var b = -1 // Decodes values directly in order if the format stores data sequentially @OptIn(ExperimentalSerializationApi::class) if (decodeSequentially()) { r = decodeIntElement(descriptor, 0) g = decodeIntElement(descriptor, 1) b = decodeIntElement(descriptor, 2) } else while (true) { // Ensures correct decoding for formats where elements may be unordered when (val index = decodeElementIndex(descriptor)) { 0 -> r = decodeIntElement(descriptor, 0) 1 -> g = decodeIntElement(descriptor, 1) 2 -> b = decodeIntElement(descriptor, 2) CompositeDecoder.DECODE_DONE -> break else -> error("Unexpected index: $index") } } require(r in 0..255 && g in 0..255 && b in 0..255) Color((r shl 16) or (g shl 8) or b) } } //sampleEnd @Serializable(ColorAsObjectSerializer::class) data class Color(val rgb: Int) fun main() { val color = Color(0x00ff00) val string = Json.encodeToString(color) println(string) // {"r":0,"g":255,"b":0} require(Json.decodeFromString<Color>(string) == color) }

Create a custom serializer for generic types

To create a custom serializer for a generic class, declare the serializer as a class instead of an object, with one KSerializer constructor parameter for each generic type parameter.

You can delegate the serialization logic for each type parameter to the corresponding KSerializer, so it's encoded according to its own serialization rules.

Let's look at an example using a generic Box<T> class:

import kotlinx.serialization.* import kotlinx.serialization.encoding.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.json.* //sampleStart @Serializable(BoxSerializer::class) data class Box<T>(val contents: T) // Creates a custom serializer as a class for Box<T> class BoxSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Box<T>> { // Defines a unique serialName for the Box<T> descriptor override val descriptor: SerialDescriptor = SerialDescriptor("my.app.Box", dataSerializer.descriptor) // Delegates serialization and deserialization override fun serialize(encoder: Encoder, value: Box<T>) = dataSerializer.serialize(encoder, value.contents) override fun deserialize(decoder: Decoder) = Box(dataSerializer.deserialize(decoder)) } @Serializable data class Project(val name: String) fun main() { val box = Box(Project("kotlinx.serialization")) val string = Json.encodeToString(box) println(string) // {"name":"kotlinx.serialization"} println(Json.decodeFromString<Box<Project>>(string)) // Box(contents=Project(name=kotlinx.serialization)) } //sampleEnd

Use a plugin-generated serializer together with a custom serializer

By default, the Kotlin serialization plugin doesn't generate a serializer if you specify a custom serializer with @Serializable(YourSerializer::class).

You might still want to use the plugin-generated serializer, for example:

  • Using the plugin-generated serializer as a fallback strategy.

  • Inspecting the plugin-generated descriptor to access the default structure.

  • Reusing default serialization behavior in subclasses that don't use a custom serializer.

You can keep the automatically generated serializer alongside your custom one by annotating the serializable class with @KeepGeneratedSerializer. To access the plugin-generated serializer, use the .generatedSerializer() function on the companion object of the serializable class.

This can also be useful when using a JsonTransformingSerializer to adjust the JSON structure and reuse the plugin-generated serializer for the default serialization logic.

Here's an example that uses both a custom serializer and the plugin-generated serializer:

import kotlinx.serialization.* import kotlinx.serialization.json.* import kotlinx.serialization.encoding.* import kotlinx.serialization.descriptors.* object ColorAsStringSerializer : KSerializer<Color> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("my.app.ColorAsString", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: Color) { val string = value.rgb.toString(16).padStart(6, '0') encoder.encodeString(string) } override fun deserialize(decoder: Decoder): Color { val string = decoder.decodeString() return Color(string.toInt(16)) } } //sampleStart @OptIn(ExperimentalSerializationApi::class) @KeepGeneratedSerializer @Serializable(ColorAsStringSerializer::class) class Color(val rgb: Int) fun main() { val green = Color(0x00ff00) // Uses the custom serializer println(Json.encodeToString(green)) // "00ff00" // Uses the plugin-generated serializer println(Json.encodeToString(Color.generatedSerializer(), green)) // {"rgb":65280} } //sampleEnd

Apply serializers

You can apply custom serializers to your own classes and to third-party types. Third-party types, such as java.util.Date, can't be directly annotated with @Serializable because their source code can't be modified.

Pass a serializer manually

To serialize a type with a custom serializer, create the serializer and pass it explicitly to overloads of functions such as Json.encodeToString() and Json.decodeFromString().

Here's an example that serializes Date values as the number of milliseconds since the Unix epoch:

import kotlinx.serialization.* import kotlinx.serialization.encoding.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.json.* import java.util.Date import java.text.SimpleDateFormat //sampleStart // Can't use @Serializable on Date without access to its source code object DateAsLongSerializer : KSerializer<Date> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("my.app.DateAsLong", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time) override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong()) } fun main() { val kotlin10ReleaseDate = SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00") // Serializes Date as a Long in milliseconds println(Json.encodeToString(DateAsLongSerializer, kotlin10ReleaseDate)) // 1455494400000 } //sampleEnd

Specify a serializer on a property

When a type is used as a property in a serializable class, specify its custom serializer on that property with the @Serializable annotation:

import kotlinx.serialization.* import kotlinx.serialization.encoding.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.json.* import java.util.Date import java.text.SimpleDateFormat object DateAsLongSerializer : KSerializer<Date> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("my.app.DateAsLong", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time) override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong()) } //sampleStart @Serializable class ProgrammingLanguage( val name: String, // Specifies the custom serializer for the Date property @Serializable(DateAsLongSerializer::class) val stableReleaseDate: Date ) fun main() { val data = ProgrammingLanguage("Kotlin", SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00")) println(Json.encodeToString(data)) // {"name":"Kotlin","stableReleaseDate":1455494400000} } //sampleEnd

Specify a serializer on a type

You can also apply the @Serializable annotation directly to a type. You can use this to specify a custom serializer for a type when it's used as a generic type argument, for example in List<Date>:

import kotlinx.serialization.* import kotlinx.serialization.encoding.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.json.* import java.util.Date import java.text.SimpleDateFormat object DateAsLongSerializer : KSerializer<Date> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("my.app.DateAsLong", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time) override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong()) } //sampleStart @Serializable class ProgrammingLanguage( val name: String, // Specifies the custom serializer for Date as a generic type argument val releaseDates: List<@Serializable(DateAsLongSerializer::class) Date> ) fun main() { val df = SimpleDateFormat("yyyy-MM-ddX") val data = ProgrammingLanguage("Kotlin", listOf(df.parse("2023-07-06+00"), df.parse("2023-04-25+00"), df.parse("2022-12-28+00"))) println(Json.encodeToString(data)) // {"name":"Kotlin","releaseDates":[1688601600000,1682380800000,1672185600000]} } //sampleEnd

Specify a serializer for a file

To apply a serializer to all properties of a given type in a source file, add the @UseSerializers annotation at the beginning of the file:

@file:UseSerializers(DateAsLongSerializer::class)

This applies the DateAsLongSerializer to all instances of that type within the file, so you don't need to annotate each property separately.

Here's an example:

// Applies the custom serializer to all properties of that type in the file @file:UseSerializers(DateAsLongSerializer::class) import kotlinx.serialization.* import kotlinx.serialization.encoding.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.json.* import java.util.Date import java.text.SimpleDateFormat object DateAsLongSerializer : KSerializer<Date> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("my.app.DateAsLong", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time) override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong()) } // Uses the file-level serializer for the Date property @Serializable class ProgrammingLanguage(val name: String, val stableReleaseDate: Date) fun main() { val data = ProgrammingLanguage("Kotlin", SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00")) println(Json.encodeToString(data)) // {"name":"Kotlin","stableReleaseDate":1455494400000} }

Specify serializers with type aliases

In Kotlin serialization, you usually specify serialization strategies explicitly with the @Serializable annotation. It doesn't provide a global serializer configuration, except for contextual serialization.

If you use the same serializer repeatedly in many places, you can define a typealias with an attached serializer annotation.

This lets you reuse the annotated type without adding the @Serializable annotation at each usage site.

Here's an example of using typealias to apply DateAsLongSerializer and DateAsSimpleTextSerializer to Date:

import kotlinx.serialization.* import kotlinx.serialization.encoding.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.json.* import java.util.Date import java.text.SimpleDateFormat import java.util.TimeZone //sampleStart object DateAsLongSerializer : KSerializer<Date> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("my.app.DateAsLong", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time) override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong()) } // Defines a serializer that encodes Date as a formatted string (yyyy-MM-dd) object DateAsSimpleTextSerializer: KSerializer<Date> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("my.app.DateAsSimpleText", PrimitiveKind.LONG) private val format = SimpleDateFormat("yyyy-MM-dd").apply { // Sets the time zone to UTC for consistent output setTimeZone(TimeZone.getTimeZone("UTC")) } override fun serialize(encoder: Encoder, value: Date) = encoder.encodeString(format.format(value)) override fun deserialize(decoder: Decoder): Date = format.parse(decoder.decodeString()) } // Applies global serializers using typealias to avoid annotating each occurrence typealias DateAsLong = @Serializable(DateAsLongSerializer::class) Date typealias DateAsText = @Serializable(DateAsSimpleTextSerializer::class) Date // Uses typealiases to apply custom serializers for Date properties @Serializable class ProgrammingLanguage(val stableReleaseDate: DateAsText, val lastReleaseTimestamp: DateAsLong) fun main() { val format = SimpleDateFormat("yyyy-MM-ddX") val data = ProgrammingLanguage(format.parse("2016-02-15+00"), format.parse("2022-07-07+00")) println(Json.encodeToString(data)) // {"stableReleaseDate":"2016-02-15","lastReleaseTimestamp":1657152000000} } //sampleEnd

Implement contextual serialization

By default, serialization strategies are defined at compile time. Contextual serialization allows you to adjust the serialization strategy for specific types at runtime, even when they're nested deep within an object tree.

For example, you can use contextual serialization to serialize java.util.Date in JSON format either as an ISO 8601 String or as a Long, depending on the protocol version being used. This approach is supported by the built-in ContextualSerializer class.

Contextual serialization selects a custom serializer for a type at runtime from a SerializersModule.

To implement contextual serialization:

  1. Mark the property in your serializable class with the @Contextual annotation.

  2. Create a SerializersModule with the SerializersModule() builder function. Use the contextual() function to register the custom serializer for contextual serialization.

    Without the SerializersModule, a SerializationException is thrown during the serialization or deserialization of contextually annotated types that don't have a default serializer.

  3. Create a Json instance and pass the SerializersModule to the serializersModule property.

Here's an example:

import kotlinx.serialization.* import kotlinx.serialization.encoding.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.json.* import java.util.Date import java.text.SimpleDateFormat //sampleStart // Creates a custom serializer for Date object DateAsLongSerializer : KSerializer<Date> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("my.app.Date", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time) override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong()) } @Serializable class ProgrammingLanguage( val name: String, // Specifies contextual serialization for Date @Contextual val stableReleaseDate: Date ) // Defines a SerializersModule and registers the contextual serializer for Date private val module = SerializersModule { contextual(DateAsLongSerializer) } // Creates a Json instance with the custom SerializersModule val format = Json { serializersModule = module } fun main() { val data = ProgrammingLanguage("Kotlin", SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00")) println(format.encodeToString(data)) // {"name":"Kotlin","stableReleaseDate":1455494400000} } //sampleEnd

Serialize generic classes contextually

To serialize generic classes contextually, you can register a function in the SerializersModule. This function receives the serializers for the generic type arguments and creates the corresponding serializer at runtime.

You can't use a single serializer instance for generic classes because generic type arguments can vary. For example, the following approach only works for Box<Int>, but not for other types like Box<String>:

val incorrectModule = SerializersModule { // This only works for Box<Int>, but not for Box<String> or other types contextual(BoxSerializer(Int.serializer())) }

Instead, register a function that creates a serializer for generic types such as Box<T> based on the serializers of their type arguments. Here's an example:

val correctModule = SerializersModule { // args[0] is the serializer for T, // for example Int.serializer() or String.serializer() contextual(Box::class) { args -> BoxSerializer(args[0]) } }

What's next

16 June 2026