The most simple and straightforward way to obtain 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:
KSerializer<MyData>
save
and load
of interfaces KSerialSaver
and KSerialLoader
serialClassDesc
of KSerializer
If you want to customize representation of the class, in most cases, you need to write your own save
and load
methods. Serial descriptor property typically used in generated version of those methods, so you likely don't need it.
You can write methods directly on companion object, and serialization plugin will respect them: (note you still have to apply annotation, because we need to define serialClassDesc
even if we don't use it)
@Serializable class MyData(val s: String) { companion object /*: KSerializer<MyData> can be omitted or not, if you like*/{ override fun save(output: KOutput, obj: MyData) { output.writeStringValue(HexConverter.printHexBinary(obj.s.toByteArray())) } override fun load(input: KInput): MyData { return MyData(String(HexConverter.parseHexBinary(input.readStringValue()))) } } }
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 library class @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 save
and load
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 save
/load
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 fun save(output: KOutput, obj: Date) { output.writeStringValue(df.format(obj)) } override fun load(input: KInput): Date { return df.parse(input.readStringValue()) } }
Due to an issue, it's impossible now to write annotations of a kind @Serializer(forClass=XXX::class)
in JavaScript now. Unfortunately, you'll have to implement all methods by yourself:
/** * MyData serializer for JavaScript */ object MyDataSerializer: KSerializer<MyData> { override fun save(output: KOutput, obj: MyData) { output.writeStringValue(HexConverter.printHexBinary(obj.s.toUtf8Bytes())) } override fun load(input: KInput): MyData { return MyData(stringFromUtf8Bytes(HexConverter.parseHexBinary(input.readStringValue()))) } override val serialClassDesc: KSerialClassDesc = SerialClassDescImpl("com.mypackage.MyData") }
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, 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 SerialContext
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. In some sense, it's similar to jackson modules.
If you want your external serializers to be used, you must register them in some (maybe new) context. Then, you have to pass the context to the framework. Contexts are flexible and can be inherited; moreover, you can pass different contexts to different instances of output formats. Let's see it in example:
// Imagine we have classes A and B with external serializers fun foo(b: B): Pair<String, ByteArray> { val aContext = SerialContext().apply { registerSerializer(A::class, ASerializer) } val strContext = SerialContext(aContext) strContext.registerSerializer(B::class, BStringSerializer) val json = JSON(context = strContext) // use string serializer for B in JSON val binContext = SerialContext(aContext) binContext.registerSerializer(B::class, BBinarySerializer) val cbor = CBOR(context = binContext) // user binarySerializer for B in CBOR return json.stringify(b) to cbor.dump(b) }
If you want to give a clue to plugin about which external serializer use for specified property, you may use 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.