summaryrefslogtreecommitdiff
path: root/docs/custom_serializers.md
blob: 79f38af0e3c109a489d4b4db1787403a7bca1ec4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# Writing and using your own serializers

The most simple and straightforward way to create serializer is to write annotation `@Serializable`
directly on your class:

```kotlin
@Serializable
class MyData(val s: String)
```

In this case, compiler plugin will generate for you:

* `.serializer()` method on companion object (will be created if there is no such yet) to obtain serializer. If your class is
a generic class, this method will have arguments `KSerializer<T1>, KSerializer<T2>`..., where `T1, T2` - your generic type parameters.
* Special nested object in your class, which implements `KSerializer<MyData>`
* Methods `serialize`, `deserialize` and `patch` of interfaces `SerializationStrategy` and `DeserializationStrategy`
* Descriptor property `descriptor` of `KSerializer`

## Table of contents

* [Customizing default serializers](#customizing)
    - [Representing classes as a single value](#representing-classes-as-a-single-value)
    - [Representing classes as multiple values](#representing-classes-as-multiple-values)
* [External serializers for library classes](#external-serializers-for-library-classes)
  + [About generic serializers](#about-generic-serializers)
* [Using custom serializers](#using-custom-serializers)
  + [`UseSerializers` annotation](#useserializers-annotation)
* [Registering and serial modules](#registering-and-context)
  + [`ContextualSerialization` annotation](#contextualserialization-annotation)

## Customizing

If you want to customize representation of the class, in most cases, you need to write your own `serialize`
and `deserialize` methods. `patch` method have default implementation of `throw UpdateNotSupportedException(descriptor.name)`. Serial descriptor property typically used in generated version of those methods; however, if you're using features like schema saving (which will be discussed later, once it's implemented), it is important to provide consistent implementation of it.

You can write methods directly on companion object, annotate it with `@Serializer(forClass = ...)`, and serialization plugin will respect it as default serializer. Note that this is applicable only for companion objects, other nested objects would not be recognized automatically.
(note you still have to apply `@Serializable` annotation, because we need to auto-generate `descriptor` even if we don't use it)

#### Representing classes as a single value
is pretty straightforward – pick corresponding descriptor, and use `encodeXXX/decodeXXX` methods of Encoder/Decoder.

```kotlin
import kotlinx.serialization.*
import kotlinx.serialization.internal.*

@Serializable
class MyData(val s: String) {
    @Serializer(forClass = MyData::class)
    companion object : KSerializer<MyData> {
        override val descriptor: SerialDescriptor =
            StringDescriptor.withName("MyData")

        override fun serialize(output: Encoder, obj: MyData) {
            output.encodeString(HexConverter.printHexBinary(obj.s.toByteArray()))
        }

        override fun deserialize(input: Decoder): MyData {
            return MyData(String(HexConverter.parseHexBinary(input.decodeString())))
        }
    }
}
```

#### Representing classes as multiple values

is a bit more complicated. In this case, you'll need to write a code which is similar to code that is generated by the plugin. Here will be presented a cut-and-paste recipe; you can read [KEEP](https://github.com/Kotlin/KEEP/blob/serialization/proposals/extensions/serialization.md) for details about core concepts.

First, we need to correctly fill-in descriptor so all formats would know about mapping from field names to indices and vice versa:

```kotlin
@Serializable
class BinaryPayload(val req: ByteArray, val res: ByteArray) {
    @Serializer(forClass = BinaryPayload::class)
    companion object : KSerializer<MyData> {
        override val descriptor: SerialDescriptor = object : SerialClassDescImpl("BinaryPayload") {
            init {
                addElement("req") // req will have index 0
                addElement("res") // res will have index 1
            }
        }
    }
}
```

Now we need to serialize class' properties one-by-one. Since they are structured, i.e. have their own position and name inside of `BinaryPayload` class, we would use `CompositeEncoder` instead of `Encoder`:

```kotlin
override fun serialize(output: Encoder, obj: BinaryPayload) {
    val compositeOutput: CompositeEncoder = output.beginStructure(descriptor)
    compositeOutput.encodeStringElement(descriptor, 0, HexConverter.printHexBinary(obj.req))
    compositeOutput.encodeStringElement(descriptor, 1, HexConverter.printHexBinary(obj.res))
    compositeOutput.endStructure(descriptor)
}
```

Deserializing a class with multiple values is a complex task, mainly because you don't know the order of fields in the input stream in advance.
So crucial part here is to make a `when` over an index of an incoming element:

```kotlin
override fun deserialize(input: Decoder): BinaryPayload {
    val inp: CompositeDecoder = input.beginStructure(descriptor)
    lateinit var req: ByteArray // consider using flags or bit mask if you 
    lateinit var res: ByteArray // need to read nullable non-optional properties
    loop@ while (true) {
        when (val i = inp.decodeElementIndex(descriptor)) {
            CompositeDecoder.READ_DONE -> break@loop
            0 -> req = HexConverter.parseHexBinary(inp.decodeStringElement(descriptor, i))
            1 -> res = HexConverter.parseHexBinary(inp.decodeStringElement(descriptor, i))
            else -> throw SerializationException("Unknown index $i")
        }
    }
    inp.endStructure(descriptor)
    return BinaryPayload(req, res)
}
```

You can see it in action [in tests](https://github.com/kotlin/kotlinx.serialization/blob/a4c41392bb735a36788db1d789ec60afdbad3ca8/runtime/common/src/test/kotlin/kotlinx/serialization/features/BinaryPayloadExampleTest.kt). Other useful examples from tests are [custom serializers which can omit some fields](https://github.com/Kotlin/kotlinx.serialization/blob/eap13/runtime/common/src/test/kotlin/kotlinx/serialization/CustomSerializersTest.kt#L67) and [custom serializer which uses ability to read JSON as tree](https://github.com/Kotlin/kotlinx.serialization/blob/master/runtime/jvm/src/test/kotlin/kotlinx/serialization/formats/json/JsonTreeAndMapperTest.kt#L35).

*Note:* this approach is not working for generic classes, see below.

## External serializers for library classes

Approach above will not work, if you can't modify source code of the class - e.g. it is a Kotlin/Java library class.
If it is Kotlin class, you can just let the plugin know you want to create serializer from object:

```kotlin
// Imagine that MyData is a third-party library class.
// Plugin will try to automatically serialize all constructor properties
// and public vars.
@Serializer(forClass = MyData::class)
object DataSerializer {}
```

This is called external serialization and imposes certain restrictions -
class should have only primary constructor's vals/vars and class body `var` properties (you can learn more in [docs](examples.md))

As in the first example, you can customize the process by overriding `serialize` and `deserialize` methods.

If it is Java class, things getting more complicated: because Java has no concept of primary constructor,
plugin don't know which properties it can took. For Java classes, you always should override `serialize`/`deserialize` methods.
You still can use `@Serializer(forClass = ...)` to generate empty serial descriptor.
For example, let's write serializer for `java.util.Date`:

```kotlin
@Serializer(forClass = Date::class)
object DateSerializer: KSerializer<Date> {
    private val df: DateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm:ss.SSS")

    override val descriptor: SerialDescriptor =
        StringDescriptor.withName("WithCustomDefault")

    override fun serialize(output: Encoder, obj: Date) {
        output.encodeString(df.format(obj))
    }

    override fun deserialize(input: Decoder): Date {
        return df.parse(input.decodeString())
    }
}
```

See it in action [here](https://github.com/kotlin/kotlinx.serialization/blob/a4c41392bb735a36788db1d789ec60afdbad3ca8/runtime/jvm/src/test/kotlin/kotlinx/serialization/features/SerializeJavaClassTest.kt).

### About generic serializers

If your class has generic type arguments, it shouldn't be an object.
It must be a class with visible primary constructor, where its arguments are `KSerializer<T0>, KSerializer<T1>, etc..` - one for each type argument of your class.

E.g. for given class `class CheckedData<T : Any>(val data: T, val checkSum: ByteArray)`, serializer would look like:

```kotlin
@Serializer(forClass = CheckedData::class)
class CheckedDataSerializer<T : Any>(val dataSerializer: KSerializer<T>) : KSerializer<CheckedData<T>>
```

If you're familiar with DI concepts, think of it as _constructor injection_ of serializers for generic properties.

Note that we haven't applied `@Serializable` on the class, because we can't customize it via companion object since companion object can't have constructor arguments.

*Current limitation*: Because primary constructor in such case is generated by the compiler itself, not the plugin,
you have to override `descriptor` manually since it can't be initialized in non-synthetic constructor.

See full sample [here](https://github.com/kotlin/kotlinx.serialization/blob/a4c41392bb735a36788db1d789ec60afdbad3ca8/runtime/jvm/src/test/kotlin/kotlinx/serialization/features/GenericCustomSerializerTest.kt). *Note*: is is broken on JS for now ([#244](https://github.com/Kotlin/kotlinx.serialization/issues/244)).

## Using custom serializers

Recommended way of using custom serializers is to give a clue to plugin about which serializer use for specified property,
using annotation in form `@Serializable(with = SomeKSerializer::class)`:

```kotlin
@Serializable
data class MyWrapper(
    val id: Int,
    @Serializable(with=MyExternalSerializer::class) val data: MyData
)
``` 

This will affect generating of `save`/`load` methods only for this dedicated class, and allows plugin to resolve serializer at compile-time to reduce runtime overhead. It would also allow plugin to inject serializers for generic properties automatically.

### `UseSerializers` annotation

If you have a lot of serializable classes, which use, say `java.util.Date`, it may be inconvenient to annotate every property with this type with `@Serializable(with=MyJavaDateSerializer::class)`. For such purpose, a file-level annotation `UseSerializers` was introduced. With it, you can write `@file:UseSerializers(MyJavaDateSerializer::class)` and all properties of type `java.util.Date` in all classes in this file would be serialized with `MyJavaDateSerializer`. See [its documentation](https://github.com/kotlin/kotlinx.serialization/blob/c6dd98c8b96a69da82ab8229665e07614296f684/runtime/common/src/main/kotlin/kotlinx/serialization/Annotations.kt#L75) for more details.

## Registering and context

By default, all serializers are resolved by plugin statically when compiling serializable class.
This gives us type-safety, performance and eliminates reflection usage to minimum. However, if there is no
`@Serializable` annotation of class and no `@Serializable(with=...)` on property, in general,
it is impossible to know at compile time which serializer to
use - user can define more than one external serializer, or define them in other module, or even it's a class from
library which doesn't know anything about serialization.

To support such cases, a concept of `SerialModule` was introduced. Roughly speaking, it's a map where
runtime part of framework is looking for serializers if they weren't resolved at compile time by plugin.
Modules are intended to be reused in different formats or even different projects
(e.g. Library A have some custom serializers and exports a module with them so Application B can use A's classes with serializers in B's output).

If you want your external serializers to be used, you pass a module with them to the serialization format.
All standard formats have constructor parameter `context: SerialModule`.

### `ContextualSerialization` annotation

When some runtime ambiguity involved, it's always better to be explicit about your intentions — especially in such a security-sensitive thing like a serialization framework. Therefore, to be able to use Context at runtime, you need to explicitly use special ContextSerializer — otherwise compiler will report you an error about missing serializer. To enable contextual serialization, simply use `@Serializable(with=ContextSerializer::class)` or `@ContextualSerialization` on a property with type which does not have default serializer. To be less verbose, it is also possible to apply this annotation on file — `@file:ContextualSerialization(A::class, B::class)` instructs compiler plugin to use ContextSerializer everywhere in this file for properties of types `A` and `B`. It also can be used on type usages: `List<@ContextualSerialization MyDate>`.

> In next releases, the same thing would be required for polymorphism and `PolymorphicSerializer`. Start using `@Polymorphic` right now!

### Example

You can also install different modules for one class into different instances of output formats. Let's see it in example:

```kotlin
// Imagine we have class Payload with 2 different serializers

val simpleModule = serializersModuleOf(Payload::class, PayloadSerializer)
// You can also create modules from map or using a builder function SerializersModule { ... }
val binaryModule = serializersModuleOf(Payload::class, BinaryPayloadSerializer)

val json1 = Json(context = simpleModule)
val json2 = Json(context = binaryModule)

// in json1, Payload would be serialized with PayloadSerializer,
// in json2, Payload would be serialized with BinaryPayloadSerializer
```

See it in action [here](https://github.com/kotlin/kotlinx.serialization/blob/a4c41392bb735a36788db1d789ec60afdbad3ca8/runtime/jvm/src/test/kotlin/kotlinx/serialization/CustomSerializersJVMTest.kt#L96).