Polymorphic serialization is usually a very complicated and dangerous feature due to the amount of reflection it brings and security concerns you should address in your application (like "what if you accidentally load or deserialize a class that is not allowed to be in this part of the program").
To overcome these drawbacks, we've focused our design on a serializers registration. It will eliminate need in cross-platform Class.forName
analog and will help avoid security problems.
So, to be able to serialize class hierarchies and restore them using a type's fully-qualified name, you should perform these steps:
SerialModule
.@Polymorphic
.Step 3 is required if you want to polymorphically serialize an open
class. If class is abstract
or interface
, @Polymorphic
annotation inferred automatically (see more in section Differences for interfaces, abstract and open classes). Moreover, if the class is sealed
, we do not need to register all subclasses manually – since they are known at compile time, compiler plugin can enumerate them automatically (see more in section Sealed classes).
Let's break down a basic case with a simple class hierarchy:
interface Message @Serializable data class StringMessage(val message: String): Message @Serializable data class IntMessage(val number: Int): Message
To be able to serialize and deserialize both StringMessage
and IntMessage
, we need the following module:
val messageModule = SerializersModule { // 1 polymorphic(Message::class) { // 2 StringMessage::class with StringMessage.serializer() // 3 IntMessage::class with IntMessage.serializer() // 4 } }
Line 1) Creates a DSL builder. Line 2) indicates that the nested block describes subclasses of Message
(you can have as many polymorphic
blocks as you'd like, for different classes). It means that these subclasses can only be serialized/deserialized when the framework encounters Message
class. In other words, this module will work with @Serializable class MessageWrapper(val m: Message)
definition and will NOT work with @Serializable class MessageWrapper(val m: Any)
. This design decision was made for security and type-safety – it encourages you to use more specific types instead of Any
.
Lines 3) and 4) register corresponding actual serializers. Again, only classes StringMessage
and IntMessage
are allowed in the stream – even if you have @Serializable class MyInternalSecretMessage: Message
, which should not be exposed to external clients, you shouldn't worry about it. Kotlinx.serialization throws an exception on an attempt to serialize or deserialize a class polymorphically if it is not registered.
The only thing left to do is to create a format instance with messageModule
:
val json = Json(context = messageModule)
After that, you can use the json
object as usual:
@Serializable data class MessageWrapper(val m: Message) json.stringify(MessageWrapper.serializer(), MessageWrapper(StringMessage("string"))) // {"m":{"type":"package.StringMessage","message":"string"}} json.stringify(MessageWrapper.serializer(), MessageWrapper(IntMessage(121))) // {"m":{"type":"package.IntMessage","number":121}}
Such an approach works on JVM, JS, and Native without reflection (only with KClass
comparison and KClass.isInstance
calls)!
Pro tip: to use
Message
without a wrapper, you can passPolymorphicSerializer(Message::class)
to parse/stringify.
By default, encoded type name is equal to class' fully-qualified name. To change that, you can annotate the class with @SerialName
annotation:
// If we have... @Serializable @SerialName("msg_number") data class IntMessage(val number: Int): Message // then... json.stringify(MessageWrapper.serializer(), MessageWrapper(IntMessage(121))) // {"m":{"type":"msg_number","number":121}}
JSON with its JsonConfiguration.Stable
and JsonConfiguration.Default
offers you to store the type name inside the object itself with the key type
. You can override key name by creating your own configuration: JsonConfiguration(classDiscriminator = "class")
or by copying an existing one: JsonConfiguration.Stable.copy(classDiscriminator = "class")
.
There is also a possibility to change type name storage location to the first element of wrapping array, i.e. to form [className, object]
:
val json = Json( configuration = JsonConfiguration(useArrayPolymorphism = true), context = messageModule ) json.stringify(MessageWrapper.serializer(), MessageWrapper(IntMessage(121))) // {"m":["msg_number",{"number":121}]}
Note: this form is default and can't be changed for formats that do not support polymorphism natively, e.g., Protobuf.
As you know, interfaces and abstract classes can't be instantiated. It also means that they can't be deserialized, and therefore, they're by default polymorphic. So if we have
interface Message
or
@Serializable abstract class Message
These declarations are equivalent:
@Serializable class MessageWrapper(val message: Message) @Serializable class MessageWrapper(@Polymorphic val message: Message)
Open classes have a state, can be instantiated and have a usual serializer. So, for
@Serializable open class Message
This declaration will use Message.serializer()
:
@Serializable class MessageWrapper(val message: Message)
And this will use PolymorphicSerializer
:
@Serializable class MessageWrapper(@Polymorphic val message: Message)
You can also make Message
class polymorphic by default by annotating the class itself:
@Serializable @Polymorphic open class Message
Sealed classes in Kotlin use the same inheritance mechanism as abstract and open ones, so kotlinx.serialization works with them in the same way, too.
However, they have one big difference: compiler knows all subclasses of sealed class anyway, therefore, the serialization plugin knows them too, and it is possible to serialize subclasses without a user's intervention correctly. Practically, it means that you do not need @Polymorphic
and SerialModule
:
@Serializable sealed class SimpleSealed { @Serializable public data class SubSealedA(val s: String) : SimpleSealed() @Serializable public data class SubSealedB(val i: Int) : SimpleSealed() } // will perform correct polymorphic serialization and deserialization: Json.stringify(SimpleSealed.serializer(), SubSealedA("foo")) // output will be // {"type":"package.SimpleSealed.SubSealedA", "s":"foo"}
You can use all polymorphism-related settings (see section A bit of customizing) and combine sealed hierarchies with abstract ones (see section Complex hierarchies with several base classes and documentation for SealedClassSerializer
).
Note that if you use @Polymorphic
on a property with a type of sealed class, it will be serialized using a regular polymorphism mechanism (which requires SerialModule
). This was an approach in previous versions of the library (see the next section).
If you, for some reason, use an older version of the library, to serialize sealed hierarchies polymorphically, you have to perform the same steps as for any other class hierarchy: make a SerialModule
, register all subclasses, etc., etc. Moreover, you should explicitly mark every usage of sealed class in @Serializable
classes as @Polymorphic
:
// @Serializable is not needed here sealed class Message { // subclasses... } val messageModule = SerializersModule { polymorphic<Message>() { // subclasses... } } @Serializable class MessageWrapper(@Polymorphic val m: Message) // @Polymorphic is required!
So, we can say that polymorphic serialization (with explicit SerialModule
) of sealed classes requires explicit opt-in on use site. To migrate to newer versions, you'll have to remove all @Polymorphic
annotations from val
s.
If you want to register subclasses for multiple base classes, e.g., in a situation like that:
interface Message @Serializable abstract class TimestampedMessage : Message { abstract val timestamp: Int } @Serializable class Wrapper( val request: Message, val response: TimestampedMessage )
You can use polymorphic
overload which accepts several base classes:
val messageModule = SerializersModule { polymorphic(Message::class, TimestampedMessage::class) { // subclasses... } }
You can even add Any::class
to this list. By doing this, you'll make possible deserialization of your classes to @Serializable class Wrapper(@Polymorphic val any: Any)
. Use this feature with caution.
If the base class itself needs serializer (in case it is open
), you can use polymorphic
overload with KSerializer<BaseClass>
in arguments or just register serializer as usual inside DSL block.
When the application grows in size, it may become inconvenient to store all class hierarchies in one serial module. And you don't have to: serial modules are composable. They even have plus
operator:
val json = Json(context = messageModule + anotherModule)
Or you can use include
in the SerializersModule {}
DSL.
If you're writing a library or shared module with an abstract class and some implementations of it, you can export your own MyLibrarySerialModule
for your clients to use so that a client can combine your module with their modules.