Note: Cases are presented here as a series of unit-tests using non-standard unquoted JSON for ease of presentation. Standards-compliant JSON is supported, too. Just replace JSON.unquoted
with plain JSON
.
-
Class constructor
val
andvar
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.unquoted.stringify(data)) assertEquals(data, JSON.parse<Data>("{a:1,b:2}")) // Serialize with external serializer for Data class @Serializer(forClass=Data::class) object ExtDataSerializer assertEquals("{a:1,b:2}", JSON.unquoted.stringify(ExtDataSerializer, data)) assertEquals(data, JSON.parse(ExtDataSerializer, "{a:1,b:2}"))
-
In case of usage of internal serialization (
@Serializable
annotation on class), both bodyval
s andvar
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.unquoted.stringify(Data(1))) assertEquals(Data(1), JSON.unquoted.parse<Data>("{a:1, b:42}"))
-
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.unquoted.parse<Data>("{a: 100500, b: 10}")
-
Initializers are called iff property is
@Transient
or@Optional
and was not read (see below).@Serializable class Data(val a: String = "42") { @Optional 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.unquoted.parse<Data>("{a: 100500, b: 10}") val data = JSON.unquoted.parse<Data>("{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 bodyvar
properties with visibility higher than protected. Bodyval
properties and all private properties are unseen for external serializer/deserializer. It also invokes all setters on bodyvar
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.unquoted.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.unquoted.stringify(Names("a", 42)))
Starting from 0.6,
@SerialName
can be used on classes, too. -
@Optional
annotation for supported properties. Note:@Optional
constructor parameters require default values, but properties with default values without annotation are treated as required.@Serializable class Data(val a: Int = 0, @Optional val b: Int = 42) { @Optional 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.unquoted.stringify(Data())) assertEquals(JSON.unquoted.parse<Data>("{a:0,b:43,c:Hello}"),Data(b = 43)) assertEquals(JSON.unquoted.parse<Data>("{a:0,b:42,c:Hello}"),Data()) assertEquals(JSON.unquoted.parse<Data>("{a:0,c:Hello}"),Data()) assertEquals(JSON.unquoted.parse<Data>("{a:0}"),Data()) // This will throw SerializationException, because 'a' is missing. JSON.unquoted.parse<Data>("{b:0}")
-
@Transient
annotation for supported properties. This annotation excludes marked properties from process of serialization or deserialization. Requires default value. Don't confuse withkotlin.jvm.Transient
!@Serializable class Data(val a: Int = 0, @Transient val b: Int = 42) { @Optional 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.unquoted.stringify(Data())) assertEquals(JSON.unquoted.parse<Data>("{a:0,c:Hello}"),Data()) assertEquals(JSON.unquoted.parse<Data>("{a:0}"),Data()) // This will throw SerializationException, because // property 'b' is unknown to deserializer. JSON.unquoted.parse<Data>("{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") { @Optional val b: String init { b = "b" } }
-
Delegates are not supported. But you can mark them as
@Transient
and they would be instantiated as usual. So this code works fine:@Serializable data class WithDelegates(val myMap: Map<String, String>) { @Transient val prop by myMap } assertEquals("value", JSON.unquoted.parse<WithDelegates>("{myMap:{prop:value}}").prop)
-
Nested values are recursively serialized, enums, primitive types, arrays, lists and maps are supported, plus other serializable classes.
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.unquoted.stringify(data)) assertEquals(data, JSON.parse<Data>("{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.unquoted.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 writeElement(desc: KSerialClassDesc, index: Int): Boolean {
val id = desc.getAnnotationsForIndex(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.getAnnotationsForClass()
method ofKSerialClassDesc
to obtain them.