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
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.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 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.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 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.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 withkotlin.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.