The most simple and straightforward way to create serializer is to write annotation @Serializable
directly on your class:
@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.KSerializer<MyData>
serialize
, deserialize
and patch
of interfaces SerializationStrategy
and DeserializationStrategy
descriptor
of KSerializer
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)
is pretty straightforward – pick corresponding descriptor, and use encodeXXX/decodeXXX
methods of Encoder/Decoder.
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(encoder: Encoder, obj: MyData) { encoder.encodeString(HexConverter.printHexBinary(obj.s.toByteArray())) } override fun deserialize(decoder: Decoder): MyData { return MyData(String(HexConverter.parseHexBinary(decoder.decodeString()))) } } }
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 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:
@Serializable class BinaryPayload(val req: ByteArray, val res: ByteArray) { @Serializer(forClass = BinaryPayload::class) companion object : KSerializer<BinaryPayload> { 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
:
override fun serialize(encoder: Encoder, obj: BinaryPayload) { val compositeOutput = encoder.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:
override fun deserialize(decoder: Decoder): BinaryPayload { val dec: CompositeDecoder = decoder.beginStructure(descriptor) var req: ByteArray? = null // consider using flags or bit mask if you var res: ByteArray? = null // need to read nullable non-optional properties loop@ while (true) { when (val i = dec.decodeElementIndex(descriptor)) { CompositeDecoder.READ_DONE -> break@loop 0 -> req = HexConverter.parseHexBinary(dec.decodeStringElement(descriptor, i)) 1 -> res = HexConverter.parseHexBinary(dec.decodeStringElement(descriptor, i)) else -> throw SerializationException("Unknown index $i") } } dec.endStructure(descriptor) return BinaryPayload( req ?: throw MissingFieldException("req"), res ?: throw MissingFieldException("res") ) }
You can see it in action in tests. Another useful example from tests is custom serializer which uses ability to read JSON as tree.
Note: this approach is not working for generic classes, see below.
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:
// 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)
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
:
@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(encoder: Encoder, obj: Date) { encoder.encodeString(df.format(obj)) } override fun deserialize(decoder: Decoder): Date { return df.parse(decoder.decodeString()) } }
See it in action here.
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:
@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.
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)
:
@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
annotationIf 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 for more details.
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
annotationWhen 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!
You can also install different modules for one class into different instances of output formats. Let's see it in example:
// 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.