Note: Cases are presented here as a series of unit-tests using non-standard unquoted JSON for ease of presentation. It was created as val json = Json(JsonConfiguration.Stable.copy(unquoted = true))
. Standards-compliant JSON is supported, too. Just use .Stable
or .Default
configurations or create your own.
Class constructor val
and var
properties. It is required for constructor to have only properties (no parameters).
@Serializable data class Data(val a: Int, val b: Int) val data = Data(1, 2) // Serialize with internal serializer for Data class assertEquals("{a:1,b:2}", json.stringify(Data.serializer(), data)) assertEquals(data, Json.parse(Data.serializer(), "{a:1,b:2}")) // Serialize with external serializer for Data class @Serializer(forClass=Data::class) object ExtDataSerializer assertEquals("{a:1,b:2}", json.stringify(ExtDataSerializer, data)) assertEquals(data, Json.parse(ExtDataSerializer, "{a:1,b:2}"))
In case of usage of internal serialization (@Serializable
annotation on class), both body val
s and var
s are supported with any visibility levels.
@Serializable class Data(val a: Int) { private val b: String = "42" override fun equals(other: Any?) = /*...*/ } assertEquals("{a:1, b:42}", json.stringify(Data.serializer(), Data(1))) assertEquals(Data(1), json.parse(Data.serializer(), "{a:1, b:42}"))
Property will be considered optional if it has default value (kotlin 1.3.30 or higher is required).
@Serializable data class Data(val a: Int, val b: Int = 42) // Serialization and deserialization with internal serializer assertEquals("{a:0,b:42}",json.stringify(Data.serializer(), Data(0))) assertEquals(json.parse(Data.serializer(), "{a:0,b:43}"),Data(0, b = 43)) assertEquals(json.parse(Data.serializer(), "{a:0,b:42}"),Data(0)) assertEquals(json.parse(Data.serializer(), "{a:0}"),Data(0)) // This will throw SerializationException, because 'a' is missing. json.parse(Data.serializer(), "{b:0}")
Tip: you can omit default values during serialization with
Json(encodeDefaults = false)
(see here).
Tip: Deprecated
@Optional
annotation was used in older version and older kotlin version.
By default, only properties which have backing fields will be serialized and restored back.
@Serializable data class Data(val a: Int) { private val b: String get() = "42" } // b is not in serialized form! assertEquals("{a:1}", json.stringify(Data.serializer(), Data(1)))
You should be careful with this, especially when you have hierarchy of serializable classes with several overrides.
Moreover, if you have several properties with the same name and different backing fields (e.g. open/override
pair), a compiler exception will be thrown. To resolve such conflicts, use @SerialName
(see below).
Important note: In this case, body properties initializers and setters are not called. So, following approach would not work:
@Serializable class Data(val a: String = "42") { val b: String = computeWithSideEffects() private fun computeWithSideEffects(): String { println("I'm a side effect") return "b" } } // prints nothing. val data = json.parse(Data.serializer(), "{a: 100500, b: 10}")
Initializers are called iff (if and only if) property is @Transient
or optional and was not read (see below).
@Serializable class Data(val a: String = "42") { val b: String = computeWithSideEffects() private fun computeWithSideEffects(): String { println("I'm a side effect") return "b" } } // prints "I'm a side effect" once. val data = json.parse(Data.serializer(), "{a: 100500, b: 10}") val data = json.parse(Data.serializer(), "{a: 100500}")
Common pattern: Validation.
Such classes are not serializable, because they have constructor parameters which are not properties:
class Data(_a: Int) { val a: Int = if ( _a >= 0) _a else throw IllegalArgumentException() }
They can be easily refactored to be used with init
blocks. init
blocks in internal deserialization, unlike initialization expressions, are always executed after all variables have been set.
@Serializable class Data(val a: Int) { init { check(a >= 0) } }
External deserialization (annotation @Serializer(forClass=...)
) has more limitations: it supports only primary constructor's vals/vars and class body var
properties with visibility higher than protected. Body val
properties and all private properties are unseen for external serializer/deserializer. It also invokes all setters on body var
s and all initialization expressions with init blocks.
It isn't supported yet in JavaScript.
class Data { var a = 0 var b = 0 val unseen = 42 override fun equals(other: Any?) = /*..*/ } val data = Data().apply { a = 1 b = 2 } // Serialize with external serializer for Data class @Serializer(forClass=Data::class) object ExtDataSerializer assertEquals("{a:1,b:2}", json.stringify(ExtDataSerializer, data)) assertEquals(data, Json.parse(ExtDataSerializer, "{a:1,b:2}"))
Having both @Serialiable class A
and @Serializer(forClass=A::class)
is possible. In this case, object marked as serializer will try to deserialize class A internally, and some strange effects may happen. But it's not exactly.
@SerialName
annotation for overriding property name with custom name in formats with name support, like JSON.
@Serializable data class Names( @SerialName("value1") val custom1: String, @SerialName("value2") val custom2: Int ) assertEquals("{value1: a, value2: 42}", json.stringify(Names.serializer(), Names("a", 42)))
Starting from 0.6,
@SerialName
can be used on classes, too.
@Required
annotation for supported properties. It makes property with default value still be mandatory and always present in serialized form.
@Serializable class Data(@Required val a: Int = 0, val b: Int = 42) { var c = "Hello" override fun equals(other: Any?) = /*...*/ } // Serialization and deserialization with internal serializer // External serializer also supported assertEquals("{a:0,b:42,c:Hello}",json.stringify(Data.serializer(), Data())) assertEquals(json.parse(Data.serializer(), "{a:0,b:43,c:Hello}"), Data(b = 43)) assertEquals(json.parse(Data.serializer(), "{a:0,b:42,c:Hello}"), Data()) assertEquals(json.parse(Data.serializer(), "{a:0,c:Hello}"), Data()) assertEquals(json.parse(Data.serializer(), "{a:0}"), Data()) // This will throw SerializationException, because 'a' is missing. json.parse(Data.serializer(), "{b:0}")
@Transient
annotation for supported properties. This annotation excludes marked properties from process of serialization or deserialization. Requires default value. Don't confuse with kotlin.jvm.Transient
!
@Serializable class Data(val a: Int = 0, @Transient val b: Int = 42) { var c = "Hello" @Transient var d = "World" override fun equals(other: Any?) = /*...*/ } // Serialization and deserialization with internal serializer // External serializer also supported assertEquals("{a:0,c:Hello}",json.stringify(Data.serializer(), Data())) assertEquals(json.parse(Data.serializer(), "{a:0,c:Hello}"), Data()) assertEquals(json.parse(Data.serializer(), "{a:0}"), Data()) // This will throw SerializationException, because // property 'b' is unknown to deserializer. json.parse(Data.serializer(), "{a:0,b:100500,c:Hello}")
Initializing @Transient
or optional fields in init blocks is not supported.
// This class is not serializable. class Data(val a: String = "42") { val b: String init { b = "b" } }
Delegates are not supported and they're by default @Transient
(since they do not have backing field), so this example works fine:
@Serializable data class WithDelegates(val myMap: Map<String, String>) { // implicit @Transient val prop by myMap } assertEquals("value", json.parse(WithDelegates.serializer(), "{myMap:{prop:value}}").prop)
Nested values are recursively serialized, enums, primitive types, arrays, lists and maps are supported, plus other serializable classes.
// Enums are implicitly @Serializable enum class TintEnum { LIGHT, DARK } @Serializable data class Data( val a: String, val b: List<Int>, val c: Map<String, TintEnum> ) val data = Data("Str", listOf(1, 2), mapOf("lt" to TintEnum.LIGHT, "dk" to TintEnum.DARK)) // Serialize with internal serializer for Data class assertEquals("{a:Str,b:[1,2],c:{lt:LIGHT,dk:DARK}}", json.stringify(Data.serializer(), data)) assertEquals(data, Json.parse("{a:Str,b:[1,2],c:{lt:LIGHT,dk:DARK}}")) // Serialize with external serializer for Data class @Serializer(forClass=Data::class) object ExtDataSerializer assertEquals("{a:Str,b:[1,2],c:{lt:LIGHT,dk:DARK}}", json.stringify(ExtDataSerializer, data)) assertEquals(data, Json.parse(ExtDataSerializer, "{a:Str,b:[1,2],c:{lt:LIGHT,dk:DARK}}"))
To obtain serializers for root-level collections, you can use extension functions defined on serializers, like .list
(see this issue)
In some cases, one may like to save additional format-specific information in the object itself. For example, protobuf field id. For this purpose, you can define your own annotation class and annotate it with @SerialInfo
:
@SerialInfo @Target(AnnotationTarget.PROPERTY) annotation class ProtoId(val id: Int) @Serializable data class MyData(@ProtoId(2) val a: Int, @ProtoId(1) val b: String)
Note that it has to be explicitly targeted to property.
Inside a process of serialization/deserialization, they are available in KSerialClassDesc
object:
override fun encodeElement(desc: SerialDescriptor, index: Int): Boolean { val id = desc.getElementAnnotations(index).filterIsInstance<ProtoId>().single().id ... }
You can apply any number of annotations with any number of arguments. Limitations: @SerialInfo
annotation class properties must have one of the following types: primitive, String, enum, or primitive array (IntArray
, BooleanArray
, etc)
Starting from 0.6,
@SerialInfo
-marked annotations can be used on classes, too. Use.getEntityAnnotations()
method ofSerialDescriptor
to obtain them.