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 vals and vars 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
@Optionalannotation 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 vars 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,
@SerialNamecan 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 ofSerialDescriptorto obtain them.