이 장은 Kotlin Serialization 가이드의 세 번째 장입니다. 이번 챕터에서는 serializer에 대해 좀 더 자세히 살펴보고 사용자 지정 serializer를 작성하는 방법에 대해 알아보겠습니다.
Serializer 소개
JSON과 같은 형식은 객체를 특정 출력 바이트로 인코딩하는 방식을 제어하지만, 객체가 구성 속성으로 분해되는 방식은 serializer에 의해 제어됩니다. 지금까지는 Serializable classes 섹션에서 설명한 것처럼 @Serializable
어노테이션을 사용하여 자동으로 생성된 serializer를 사용하거나 Builtin classes 섹션에서 설명한 빌트인 serializer를 사용해왔습니다.
Serializer를 소개하기 위해 rgb
바이트를 저장하는 정수 값을 가지는 Color
클래스를 예로 들어보겠습니다.
기본적으로 이 클래스는 rgb
프로퍼티를 JSON으로 직렬화합니다.
{"rgb":65280}
플러그인이 생성한 serializer
이전 예제의 Color
클래스처럼 @Serializable
어노테이션이 표시된 모든 클래스는 Kotlin 직렬화 컴파일러 플러그인에 의해 자동으로 생성된 KSerializer 인터페이스의 인스턴스를 가져옵니다. 클래스의 컴패니언 객체에서 .serializer()
함수를 사용하여 이 인스턴스를 검색할 수 있습니다.
직렬화된 클래스의 구조를 설명하는 descriptor 속성을 살펴볼 수도 있습니다. 이에 대한 자세한 내용은 다음 섹션에서 알아보겠습니다.
이 serializer는 Color
클래스가 자체적으로 직렬화되거나 또는 다른 클래스의 프로퍼티로 사용될 때 Kotlin 직렬화 프레임워크가 자동으로 찾아서 사용합니다.
Info
직렬화가 가능한 클래스의 컴패니언 객체에는 자체 함수
serializer()
를 정의할 수 없습니다.
플러그인이 생성한 제네릭 serializer
Generic classes에서 본 Box
클래스와 같은 제네릭 클래스의 경우, 자동으로 생성된 .serializer()
함수는 해당 클래스의 타입 파라미터의 갯수만큼 파라미터를 받습니다. 이러한 파라미터는 KSerializer 타입이므로 제네릭 클래스에 대한 serializer 인스턴스를 생성할 때 실제 타입 인수의 serializer를 제공해야 합니다.
예제처럼 구체적인 Box<Color>
를 직렬화하기 위해 serializer가 인스턴스화되었습니다.
Box(contents: Color)
빌트인된 원시(primitive) serializer
primitive builtin classes에 대한 serizlizer는 .serializer()
확장 함수를 사용하여 찾을 수 있습니다.
컬렉션 serializer 만들기
빌트인 컬렉션에 대한 serializer가 필요한 경우 ListSerializer(), SetSerializer(), MapSerializer() 등을 사용하여 명시적으로 구현하여야 합니다. 이러한 클래스는 제네릭이므로 serializer를 인스턴스화하려면 해당 타입 매개변수에 맞는 serializer를 제공해야 합니다. 예를 들어 다음과 같은 방법으로 List<String>
에 대한 serializer를 생성할 수 있습니다.
최상위 serializer 함수 사용하기
잘모르겠으면 언제든지 최상위 제네릭 serializer<T>()
함수를 사용하여 소스 코드에서 임의의 Kotlin 타입에 대한 serializer를 검색할 수 있습니다.
사용자정의 serializer
플러그인이 생성한 serializer는 사용하기에는 편리하지만 Color
와 같은 클래스에 대해 원하는 JSON을 생성하지 못할 수 있습니다. 이제 대안을 찾아보겠습니다.
원시(Primitive) serializer
녹색이 "00ff00"
으로 표시된 16진수 문자열로 Color
클래스를 직렬화하려고 합니다. 이를 위해 Color
클래스에 대한 KSerializer 인터페이스를 구현하는 객체를 작성합니다.
object ColorAsStringSerializer : KSerializer<Color> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Color", 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))
}
}
Serializer는 다음의 세 가지 필수 요소가 있습니다.
serialize
함수는SerializationStrategy
를 구현합니다. 이 함수는Encoder
인스턴스와 직렬화할 값을 받습니다. 이 함수는 값을 원시(primitive) 시퀀스로 표현하기 위해Encoder
의encodeXxx
함수를 사용합니다.encodeXxx
함수는 직렬화에서 지원하는 각 원시타입별로 있습니다. 위의 예제에서는encodeString
함수를 사용하였습니다.deserialize
함수는DeserializationStrategy
를 구현합니다. 이 함수는Decoder
인스턴스를 받아 역직렬화된 값을 리턴합니다. 이 함수는Encoder
와 반대 동작을 하는Decoder
의decodeXxx
함수를 사용합니다. 위의 예제에서는decodeString
함수를 사용하였습니다.descriptor
프로퍼티는 포맷 구현에서 해당 함수가 호출하는 인코딩/디코딩 메서드를 미리 알 수 있도록encodeXxx
와decodeXxx
함수가 정확히 무엇을 하는지 충실히 설명해야 합니다. 일부 포맷은 직렬화된 데이터의 스키마를 생성하는 데 사용할 수도 있습니다. 원시 직렬화의 경우 직렬화되는 타입의 고유한 이름과 함께PrimitiveSerialDescriptor
함수를 사용해야 합니다.PrimitiveKind
는 구현에 사용된 특정encodeXxx
/decodeXxx
메서드에 대해 설명합니다.
Info
descriptor
가 인코딩/디코딩 메서드와 일치하지 않는 경우, 결과 코드의 동작이 지정되지 않고 향후 업데이트에서 임의로 변경될 수 있습니다.
다음 단계는 serializer를 클래스에 바인딩하는 것입니다. @Serializable
어노테이션에 with
프로퍼티를 추가하여 수행합니다.
@Serializable(with = ColorAsStringSerializer::class)
class Color(val rgb: Int)
이제 이전처럼 Color
클래스를 직렬화할 수 있습니다.
fun main() {
val green = Color(0x00ff00)
println(Json.encodeToString(green))
}
전체 코드는 다음과 같으며, 원하는 16진수 문자열로 직렬화된 표현을 얻을 수 있습니다.
역직렬화 역시 deserialize
메서드를 구현했기 때문에 간단히 동작합니다.
Color
프로퍼티를 가지는 다른 클래스를 직렬화/역직렬화하는 경우에도 잘 동작합니다.
결과는 두 Color
프로퍼티 모두 문자열로 잘 직렬화 되었습니다.
{"background":"ffffff","foreground":"000000"}
Delegating serializers
In the previous example, we represented the Color
class as a string. String is considered to be a primitive type, therefore we used PrimitiveClassDescriptor
and specialized encodeString
method. Now let’s see what our actions would be if we have to serialize Color
as another non-primitive type, let’s say IntArray
.
An implementation of KSerializer for our original Color
class is going to perform a conversion between Color
and IntArray
, but delegate the actual serialization logic to IntArraySerializer
using encodeSerializableValue and decodeSerializableValue.
import kotlinx.serialization.builtins.IntArraySerializer
class ColorIntArraySerializer : KSerializer<Color> {
private val delegateSerializer = IntArraySerializer()
@OptIn(ExperimentalSerializationApi::class)
override val descriptor = SerialDescriptor("Color", delegateSerializer.descriptor)
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)
}
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])
}
}
Note that we can’t use default Color.serializer().descriptor
here because formats that rely on the schema may think that we would call encodeInt
instead of encodeSerializableValue
. Neither we can use IntArraySerializer().descriptor
directly — otherwise, formats that handle int arrays specially can’t tell if value
is really an IntArray
or a Color
. Don’t worry, this optimization would still kick in when serializing the actual underlying int array.
An example of how a format can treat arrays specially is shown in the formats guide.
Now we can use the serializer:
@Serializable(with = ColorIntArraySerializer::class)
class Color(val rgb: Int)
fun main() {
val green = Color(0x00ff00)
println(Json.encodeToString(green))
}
As you can see, such array representation is not very useful in JSON, but may save some space when used with a ByteArray
and a binary format.
You can get the full code here.
Composite serializer via surrogate
Now our challenge is to get Color
serialized so that it is represented in JSON as if it is a class with three properties—r
, g
, and b
—so that JSON encodes it as an object. The easiest way to achieve this is to define a surrogate class mimicking the serialized form of Color
that we are going to use for its serialization. We also set the SerialName of this surrogate class to Color
. Then if any format uses this name the surrogate looks like it is a Color
class. The surrogate class can be private
, and can enforce all the constraints on the serial representation of the class in its init
block.
@Serializable
@SerialName("Color")
private class ColorSurrogate(val r: Int, val g: Int, val b: Int) {
init {
require(r in 0..255 && g in 0..255 && b in 0..255)
}
}
An example of where the class name is used is shown in the Custom subclass serial name section in the chapter on polymorphism.
Now we can use the ColorSurrogate.serializer()
function to retrieve a plugin-generated serializer for the surrogate class.
We can use the same approach as in delegating serializer, but this time, we are fully reusing an automatically generated SerialDescriptor for the surrogate because it should be indistinguishable from the original.
object ColorSerializer : KSerializer<Color> {
override val descriptor: SerialDescriptor = ColorSurrogate.serializer().descriptor
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)
}
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)
}
}
We bind the ColorSerializer
serializer to the Color
class.
@Serializable(with = ColorSerializer::class)
class Color(val rgb: Int)
Now we can enjoy the result of serialization for the Color
class.
You can get the full code here.
Handwritten composite serializer
There are some cases where a surrogate solution does not fit. Perhaps we want to avoid the performance implications of additional allocation, or we want a configurable/dynamic set of properties for the resulting serial representation. In these cases we need to manually write a class serializer which mimics the behaviour of a generated serializer.
object ColorAsObjectSerializer : KSerializer<Color> {
Let’s introduce it piece by piece. First, a descriptor is defined using the buildClassSerialDescriptor builder. The element function in the builder DSL automatically fetches serializers for the corresponding fields by their type. The order of elements is important. They are indexed starting from zero.
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("Color") {
element<Int>("r")
element<Int>("g")
element<Int>("b")
}
The “element” is a generic term here. What is an element of a descriptor depends on its SerialKind. Elements of a class descriptor are its properties, elements of a enum descriptor are its cases, etc.
Then we write the serialize
function using the encodeStructure DSL that provides access to the CompositeEncoder in its block. The difference between Encoder and CompositeEncoder is the latter has encodeXxxElement
functions that correspond to the encodeXxx
functions of the former. They must be called in the same order as 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)
}
The most complex piece of code is the deserialize
function. It must support formats, like JSON, that can decode properties in an arbitrary order. It starts with the call to decodeStructure to get access to a CompositeDecoder. Inside it we write a loop that repeatedly calls decodeElementIndex to decode the index of the next element, then we decode the corresponding element using decodeIntElement in our example, and finally we terminate the loop when CompositeDecoder.DECODE_DONE
is encountered.
override fun deserialize(decoder: Decoder): Color =
decoder.decodeStructure(descriptor) {
var r = -1
var g = -1
var b = -1
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)
}
Now we bind the resulting serializer to the Color
class and test its serialization/deserialization.
@Serializable(with = ColorAsObjectSerializer::class)
data class Color(val rgb: Int)
fun main() {
val color = Color(0x00ff00)
val string = Json.encodeToString(color)
println(string)
require(Json.decodeFromString<Color>(string) == color)
}
You can get the full code here.
As before, we got the Color
class represented as a JSON object with three keys:
Sequential decoding protocol (experimental)
The implementation of the deserialize
function from the previous section works with any format. However, some formats either always store all the complex data in order or only do so sometimes (JSON always stores collections in order). With these formats the complex protocol of calling decodeElementIndex
in a loop is unnecessary, and a faster implementation can be used if the CompositeDecoder.decodeSequentially function returns true
. The plugin-generated serializers are actually conceptually similar to the code below.
override fun deserialize(decoder: Decoder): Color =
decoder.decodeStructure(descriptor) {
var r = -1
var g = -1
var b = -1
@OptIn(ExperimentalSerializationApi::class)
if (decodeSequentially()) { // sequential decoding protocol
r = decodeIntElement(descriptor, 0)
g = decodeIntElement(descriptor, 1)
b = decodeIntElement(descriptor, 2)
} else 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)
}
You can get the full code here.
Serializing 3rd party classes
Sometimes an application has to work with an external type that is not serializable. Let us use java.util.Date as an example. As before, we start by writing an implementation of KSerializer for the class. Our goal is to get a Date
serialized as a long number of milliseconds following the approach from the Primitive serializer section.
In the following sections any kind of
Date
serializer would work. For example, if we wantDate
to be serialized as an object, we would use an approach from the Composite serializer via surrogate section.
See also Deriving external serializer for another Kotlin class (experimental) when you need to serialize a 3rd-party Kotlin class that could have been serializable, but is not.
object DateAsLongSerializer : KSerializer<Date> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG)
override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time)
override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong())
}
We cannot bind the DateAsLongSerializer
serializer to the Date
class with the @Serializable
annotation because we don’t control the Date
source code. There are several ways to work around that.
Passing a serializer manually
The encodeToXxx
and decodeFromXxx
functions offer overloaded versions that accept either a SerializationStrategy or DeserializationStrategy as their first parameter, respectively. This feature allows you to provide a custom serializer for types that aren’t annotated with @Serializable
by default.
This approach is particularly useful when working with non-serializable classes like Date
as the top-level object being serialized. Here’s an example:
fun main() {
val kotlin10ReleaseDate = SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00")
println(Json.encodeToString(DateAsLongSerializer, kotlin10ReleaseDate))
}
You can get the full code here.
Specifying a serializer on a property
When a property of a non-serializable class, like Date
, is serialized as part of a serializable class we must supply its serializer or the code will not compile. This is accomplished using the @Serializable
annotation on the property.
@Serializable
class ProgrammingLanguage(
val name: String,
@Serializable(with = DateAsLongSerializer::class)
val stableReleaseDate: Date
)
fun main() {
val data = ProgrammingLanguage("Kotlin", SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00"))
println(Json.encodeToString(data))
}
You can get the full code here.
The stableReleaseDate
property is serialized with the serialization strategy that we specified for it:
{"name":"Kotlin","stableReleaseDate":1455494400000}
Specifying a serializer for a particular type
@Serializable
annotation can also be applied directly to the types. This is handy when a class that requires a custom serializer, such as Date
, happens to be a generic type argument. The most common use case for that is when you have a list of dates:
@Serializable
class ProgrammingLanguage(
val name: String,
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))
}
You can get the full code here.
{"name":"Kotlin","releaseDates":[1688601600000,1682380800000,1672185600000]}
Specifying serializers for a file
A serializer for a specific type, like Date
, can be specified for a whole source code file with the file-level UseSerializers annotation at the beginning of the file.
@file:UseSerializers(DateAsLongSerializer::class)
Now a Date
property can be used in a serializable class without additional annotations.
@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))
}
You can get the full code here.
{"name":"Kotlin","stableReleaseDate":1455494400000}
Specifying a serializer globally using a typealias
kotlinx.serialization tends to be the always-explicit framework when it comes to serialization strategies: normally, they should be explicitly mentioned in @Serializable
annotation. Therefore, we do not provide any kind of global serializer configuration (except for context serializer mentioned later).
However, in projects with a large number of files and classes, it may be too cumbersome to specify @file:UseSerializers
every time, especially for classes like Date
or Instant
that have a fixed strategy of serialization across the project. For such cases, it is possible to specify serializers using typealias
es, as they preserve annotations, including serialization-related ones:
typealias DateAsLong = @Serializable(DateAsLongSerializer::class) Date
typealias DateAsText = @Serializable(DateAsSimpleTextSerializer::class) Date
Using these new different types, it is possible to serialize a Date differently without additional annotations:
@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))
}
You can get the full code here.
{"stableReleaseDate":"2016-02-15","lastReleaseTimestamp":1657152000000}
Custom serializers for a generic type
Let us take a look at the following example of the generic Box<T>
class. It is marked with @Serializable(with = BoxSerializer::class)
as we plan to have a custom serialization strategy for it.
@Serializable(with = BoxSerializer::class)
data class Box<T>(val contents: T)
An implementation of KSerializer for a regular type is written as an object
, as we saw in this chapter’s examples for the Color
type. A generic class serializer is instantiated with serializers for its generic parameters. We saw this in the Plugin-generated generic serializer section. A custom serializer for a generic class must be a class
with a constructor that accepts as many KSerializer parameters as the type has generic parameters. Let us write a Box<T>
serializer that erases itself during serialization, delegating everything to the underlying serializer of its data
property.
class BoxSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Box<T>> {
override val descriptor: SerialDescriptor = dataSerializer.descriptor
override fun serialize(encoder: Encoder, value: Box<T>) = dataSerializer.serialize(encoder, value.contents)
override fun deserialize(decoder: Decoder) = Box(dataSerializer.deserialize(decoder))
}
Now we can serialize and deserialize Box<Project>
.
@Serializable
data class Project(val name: String)
fun main() {
val box = Box(Project("kotlinx.serialization"))
val string = Json.encodeToString(box)
println(string)
println(Json.decodeFromString<Box<Project>>(string))
}
You can get the full code here.
The resulting JSON looks like the Project
class was serialized directly.
{"name":"kotlinx.serialization"}
Box(contents=Project(name=kotlinx.serialization))
Format-specific serializers
The above custom serializers worked in the same way for every format. However, there might be format-specific features that a serializer implementation would like to take advantage of.
- The Json transformations section of the Json chapter provides examples of serializers that utilize JSON-specific features.
- A format implementation can have a format-specific representation for a type as explained in the Format-specific types section of the Alternative and custom formats (experimental) chapter.
This chapter proceeds with a generic approach to tweaking the serialization strategy based on the context.
Simultaneous use of plugin-generated and custom serializers
In some cases it may be useful to have a serialization plugin continue to generate a serializer even if a custom one is used for the class.
The most common examples are: using a plugin-generated serializer for fallback strategy, accessing type structure via descriptor of plugin-generated serializer, use default serialization behavior in descendants that do not use custom serializers.
In order for the plugin to continue generating the serializer, you must specify the @KeepGeneratedSerializer
annotation in the type declaration. In this case, the serializer will be accessible using the .generatedSerializer()
function on the class’s companion object.
This annotation is currently experimental. Kotlin 2.0.20 or higher is required for this feature to work.
Annotation @KeepGeneratedSerializer
is not allowed on classes involved in polymorphic serialization: interfaces, sealed classes, abstract classes, classes marked by Polymorphic.
An example of using two serializers at once:
@OptIn(ExperimentalSerializationApi::class)
@KeepGeneratedSerializer
@Serializable(with = ColorAsStringSerializer::class)
class Color(val rgb: Int)
fun main() {
val green = Color(0x00ff00)
println(Json.encodeToString(green))
println(Json.encodeToString(Color.generatedSerializer(), green))
}
You can get the full code here.
As a result, serialization will occur using custom and plugin-generated serializers:
Contextual serialization
All the previous approaches to specifying custom serialization strategies were static, that is fully defined at compile-time. The exception was the Passing a serializer manually approach, but it worked only on a top-level object. You might need to change the serialization strategy for objects deep in the serialized object tree at run-time, with the strategy being selected in a context-dependent way. For example, you might want to represent java.util.Date
in JSON format as an ISO 8601 string or as a long integer depending on a version of a protocol you are serializing data for. This is called contextual serialization, and it is supported by a built-in ContextualSerializer class. Usually we don’t have to use this serializer class explicitly—there is the Contextual annotation providing a shortcut to the @Serializable(with = ContextualSerializer::class)
annotation, or the UseContextualSerialization annotation can be used at the file-level just like the UseSerializers annotation. Let’s see an example utilizing the former.
@Serializable
class ProgrammingLanguage(
val name: String,
@Contextual
val stableReleaseDate: Date
)
To actually serialize this class we must provide the corresponding context when calling the encodeToXxx
/decodeFromXxx
functions. Without it we’ll get a “Serializer for class ‘Date’ is not found” exception.
See here for an example that produces that exception.
Serializers module
To provide a context, we define a SerializersModule instance that describes which serializers shall be used at run-time to serialize which contextually-serializable classes. This is done using the SerializersModule {} builder function, which provides the SerializersModuleBuilder DSL to register serializers. In the example below we use the contextual function with the serializer. The corresponding class this serializer is defined for is fetched automatically via the reified
type parameter.
private val module = SerializersModule {
contextual(DateAsLongSerializer)
}
Next we create an instance of the Json format with this module using the Json {} builder function and the serializersModule property.
Details on custom JSON configurations can be found in the JSON configuration section.
val format = Json { serializersModule = module }
Now we can serialize our data with this format
.
fun main() {
val data = ProgrammingLanguage("Kotlin", SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00"))
println(format.encodeToString(data))
}
You can get the full code here.
{"name":"Kotlin","stableReleaseDate":1455494400000}
Contextual serialization and generic classes
In the previous section we saw that we can register serializer instance in the module for a class we want to serialize contextually. We also know that serializers for generic classes have constructor parameters — type arguments serializers. It means that we can’t use one serializer instance for a class if this class is generic:
val incorrectModule = SerializersModule {
// Can serialize only Box<Int>, but not Box<String> or others
contextual(BoxSerializer(Int.serializer()))
}
For cases when one want to serialize contextually a generic class, it is possible to register provider in the module:
val correctModule = SerializersModule {
// args[0] contains Int.serializer() or String.serializer(), depending on the usage
contextual(Box::class) { args -> BoxSerializer(args[0]) }
}
Additional details on serialization modules are given in the Merging library serializers modules section of the Polymorphism chapter.
Deriving external serializer for another Kotlin class (experimental)
If a 3rd-party class to be serialized is a Kotlin class with a properties-only primary constructor, a kind of class which could have been made @Serializable
, then you can generate an external serializer for it using the Serializer annotation on an object with the forClass
property.
// NOT @Serializable
class Project(val name: String, val language: String)
@OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = Project::class)
object ProjectSerializer
You must bind this serializer to a class using one of the approaches explained in this chapter. We’ll follow the Passing a serializer manually approach for this example.
fun main() {
val data = Project("kotlinx.serialization", "Kotlin")
println(Json.encodeToString(ProjectSerializer, data))
}
You can get the full code here.
This gets all the Project
properties serialized:
{"name":"kotlinx.serialization","language":"Kotlin"}
External serialization uses properties
As we saw earlier, the regular @Serializable
annotation creates a serializer so that Backing fields are serialized. External serialization using Serializer(forClass = ...)
has no access to backing fields and works differently. It serializes only accessible properties that have setters or are part of the primary constructor. The following example shows this.
// NOT @Serializable, will use external serializer
class Project(
// val in a primary constructor -- serialized
val name: String
) {
var stars: Int = 0 // property with getter & setter -- serialized
val path: String // getter only -- not serialized
get() = "kotlin/$name"
private var locked: Boolean = false // private, not accessible -- not serialized
}
@OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = Project::class)
object ProjectSerializer
fun main() {
val data = Project("kotlinx.serialization").apply { stars = 9000 }
println(Json.encodeToString(ProjectSerializer, data))
}
You can get the full code here.
The output is shown below.
{"name":"kotlinx.serialization","stars":9000}
The next chapter covers Polymorphism