I have a data class structure that I'd like serialize/deserialize. It contains a class, of which I'd like to only serialize an id ignoring the rest of the data, and during deserialization load the rest of the data from a repository using the id. Something like this:
@Serializable
data class Thing(
val id: String,
val name: String
// etc.
)
class ThingSerializer(private val thingRepository: ThingRepository) : KSerializer<Thing> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Thing", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Thing) {
encoder.encodeString(value.id)
}
override fun deserialize(decoder: Decoder): Thing {
val id = decoder.decodeString()
return thingRepository.findById(id)!!
}
}
Is there a way to create an instance of this serializer and configure the SerializationModule
to use it?
Note first that Kotlin serialization is designed around serializers that are defined solely at compile time as this provides "type-safety, performance, and avoids reflection usage"1. Hence why the framework is centred around class definitions for serializers.
Bearing that in mind, there are two ways of solving the problem of providing parameters to a serializer:
ThingRepository
) statically; orWith this approach you could write your serializer with a static field instead, ie:
class ThingSerializer : KSerializer<Thing> {
companion object {
var thingRepository: ThingRepository? = null
}
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Thing", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Thing) {
encoder.encodeString(value.id)
}
override fun deserialize(decoder: Decoder): Thing {
val id = decoder.decodeString()
return thingRepository!!.findById(id)!!
}
}
You can tell the plugin about the serializer by annotating the target class:
@Serializable(with = ThingSerializer::class)
data class Thing
You would also have to set the static field prior to serialization:
ThingSerializer.thingRepository = thingRepository
This approach allows you to register a serializer instance in the serializers module, rather than attach a class, like so:
val serializersModule = SerializersModule {
contextual(ThingSerializer(thingRepository))
}
You also need to tell the plugin at compile time that you want to use the contextual serializer for that class. There are several ways to do that, depending on your taste:
This works but the compiler raises a warning because you are using a serializer for type Any
on a specific class. Hence it's necessary to suppress the warning:
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@OptIn(ExperimentalSerializationApi::class)
@Serializable(with = ContextualSerializer::class)
data class Thing
@Contextual
If you prefer not have to suppress a warning, you can annotate every occurrence of your target class with @Contextual
when it occurs. Of course the downside of that is you have to include that annotation everywhere.
@Serializable
class ThingUser(@Contextual private val thing: Thing)
@UseContextualSerialization
There is also a file-level annotation you can use:
@file:UseContextualSerialization(Thing::class)
package com.thing
class ThingUserTwo(private val thing: Thing)
Finally, you would have to remember to provide the serializers module when you create your Json
instance (or equivalent):
val json = Json { serializersModule = serializersModule }
1See the serialization KEEP. Presumably the framework instantiates the serializers at compile time when performing code generation.