# Polymorphism This is the fourth chapter of the [Kotlin Serialization Guide](serialization-guide.md). In this chapter we'll see how Kotlin Serialization deals with polymorphic class hierarchies. **Table of contents** * [Closed polymorphism](#closed-polymorphism) * [Static types](#static-types) * [Designing serializable hierarchy](#designing-serializable-hierarchy) * [Sealed classes](#sealed-classes) * [Custom subclass serial name](#custom-subclass-serial-name) * [Concrete properties in a base class](#concrete-properties-in-a-base-class) * [Objects](#objects) * [Open polymorphism](#open-polymorphism) * [Registered subclasses](#registered-subclasses) * [Serializing interfaces](#serializing-interfaces) * [Property of an interface type](#property-of-an-interface-type) * [Static parent type lookup for polymorphism](#static-parent-type-lookup-for-polymorphism) * [Explicitly marking polymorphic class properties](#explicitly-marking-polymorphic-class-properties) * [Registering multiple superclasses](#registering-multiple-superclasses) * [Polymorphism and generic classes](#polymorphism-and-generic-classes) * [Merging library serializers modules](#merging-library-serializers-modules) * [Default polymorphic type handler for deserialization](#default-polymorphic-type-handler-for-deserialization) * [Default polymorphic type handler for serialization](#default-polymorphic-type-handler-for-serialization) ## Closed polymorphism Let us start with basic introduction to polymorphism. ### Static types Kotlin Serialization is fully static with respect to types by default. The structure of encoded objects is determined by *compile-time* types of objects. Let's examine this aspect in more detail and learn how to serialize polymorphic data structures, where the type of data is determined at runtime. To show the static nature of Kotlin Serialization let us make the following setup. An `open class Project` has just the `name` property, while its derived `class OwnedProject` adds an `owner` property. In the below example, we serialize `data` variable with a static type of `Project` that is initialized with an instance of `OwnedProject` at runtime. ```kotlin @Serializable open class Project(val name: String) class OwnedProject(name: String, val owner: String) : Project(name) fun main() { val data: Project = OwnedProject("kotlinx.coroutines", "kotlin") println(Json.encodeToString(data)) } ``` > You can get the full code [here](../guide/example/example-poly-01.kt). Despite the runtime type of `OwnedProject`, only the `Project` class properties are getting serialized. ```text {"name":"kotlinx.coroutines"} ``` Let's change the compile-time type of `data` to `OwnedProject`. ```kotlin @Serializable open class Project(val name: String) class OwnedProject(name: String, val owner: String) : Project(name) fun main() { val data = OwnedProject("kotlinx.coroutines", "kotlin") println(Json.encodeToString(data)) } ``` > You can get the full code [here](../guide/example/example-poly-02.kt). We get an error, because the `OwnedProject` class is not serializable. ```text Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'OwnedProject' is not found. Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied. ``` ### Designing serializable hierarchy We cannot simply mark `OwnedProject` from the previous example as `@Serializable`. It does not compile, running into the [constructor properties requirement](basic-serialization.md#constructor-properties-requirement). To make hierarchy of classes serializable, the properties in the parent class have to be marked `abstract`, making the `Project` class `abstract`, too. ```kotlin @Serializable abstract class Project { abstract val name: String } class OwnedProject(override val name: String, val owner: String) : Project() fun main() { val data: Project = OwnedProject("kotlinx.coroutines", "kotlin") println(Json.encodeToString(data)) } ``` > You can get the full code [here](../guide/example/example-poly-03.kt). This is close to the best design for a serializable hierarchy of classes, but running it produces the following error: ```text Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for subclass 'OwnedProject' is not found in the polymorphic scope of 'Project'. Check if class with serial name 'OwnedProject' exists and serializer is registered in a corresponding SerializersModule. To be registered automatically, class 'OwnedProject' has to be '@Serializable', and the base class 'Project' has to be sealed and '@Serializable'. ``` ### Sealed classes The most straightforward way to use serialization with a polymorphic hierarchy is to mark the base class `sealed`. _All_ subclasses of a sealed class must be explicitly marked as `@Serializable`. ```kotlin @Serializable sealed class Project { abstract val name: String } @Serializable class OwnedProject(override val name: String, val owner: String) : Project() fun main() { val data: Project = OwnedProject("kotlinx.coroutines", "kotlin") println(Json.encodeToString(data)) // Serializing data of compile-time type Project } ``` > You can get the full code [here](../guide/example/example-poly-04.kt). Now we can see a default way to represent polymorphism in JSON. A `type` key is added to the resulting JSON object as a _discriminator_. ```text {"type":"example.examplePoly04.OwnedProject","name":"kotlinx.coroutines","owner":"kotlin"} ``` Pay attention to the small, but very important detail in the above example that is related to [Static types](#static-types): the `val data` property has a compile-time type of `Project`, even though its run-time type is `OwnedProject`. When serializing polymorphic class hierarchies you must ensure that the compile-time type of the serialized object is a polymorphic one, not a concrete one. Let us see what happens if the example is slightly changed, so that the compile-time of the object that is being serialized is `OwnedProject` (the same as its run-time type). ```kotlin @Serializable sealed class Project { abstract val name: String } @Serializable class OwnedProject(override val name: String, val owner: String) : Project() fun main() { val data = OwnedProject("kotlinx.coroutines", "kotlin") // data: OwnedProject here println(Json.encodeToString(data)) // Serializing data of compile-time type OwnedProject } ``` > You can get the full code [here](../guide/example/example-poly-05.kt). The type of `OwnedProject` is concrete and is not polymorphic, thus the `type` discriminator property is not emitted into the resulting JSON. ```text {"name":"kotlinx.coroutines","owner":"kotlin"} ``` In general, Kotlin Serialization is designed to work correctly only when the compile-time type used during serialization is the same one as the compile-time type used during deserialization. You can always specify the type explicitly when calling serialization functions. The previous example can be corrected to use `Project` type for serialization by calling `Json.encodeToString(data)`. ### Custom subclass serial name A value of the `type` key is a fully qualified class name by default. We can put [SerialName] annotation onto the corresponding class to change it. ```kotlin @Serializable sealed class Project { abstract val name: String } @Serializable @SerialName("owned") class OwnedProject(override val name: String, val owner: String) : Project() fun main() { val data: Project = OwnedProject("kotlinx.coroutines", "kotlin") println(Json.encodeToString(data)) } ``` > You can get the full code [here](../guide/example/example-poly-06.kt). This way we can have a stable _serial name_ that is not affected by the class's name in the source code. ```text {"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"} ``` > In addition to that, JSON can be configured to use a different key name for the class discriminator. > You can find an example in the [Class discriminator for polymorphism](json.md#class-discriminator-for-polymorphism) section. ### Concrete properties in a base class A base class in a sealed hierarchy can have properties with backing fields. ```kotlin @Serializable sealed class Project { abstract val name: String var status = "open" } @Serializable @SerialName("owned") class OwnedProject(override val name: String, val owner: String) : Project() fun main() { val json = Json { encodeDefaults = true } // "status" will be skipped otherwise val data: Project = OwnedProject("kotlinx.coroutines", "kotlin") println(json.encodeToString(data)) } ``` > You can get the full code [here](../guide/example/example-poly-07.kt). The properties of the superclass are serialized before the properties of the subclass. ```text {"type":"owned","status":"open","name":"kotlinx.coroutines","owner":"kotlin"} ``` ### Objects Sealed hierarchies can have objects as their subclasses and they also need to be marked as `@Serializable`. Let's take a different example with a hierarchy of `Response` classes. ```kotlin @Serializable sealed class Response @Serializable object EmptyResponse : Response() @Serializable class TextResponse(val text: String) : Response() ``` Let us serialize a list of different responses. ```kotlin fun main() { val list = listOf(EmptyResponse, TextResponse("OK")) println(Json.encodeToString(list)) } ``` > You can get the full code [here](../guide/example/example-poly-08.kt). An object serializes as an empty class, also using its fully-qualified class name as type by default: ```text [{"type":"example.examplePoly08.EmptyResponse"},{"type":"example.examplePoly08.TextResponse","text":"OK"}] ``` > Even if object has properties, they are not serialized. ## Open polymorphism Serialization can work with arbitrary `open` classes or `abstract` classes. However, since this kind of polymorphism is open, there is a possibility that subclasses are defined anywhere in the source code, even in other modules, the list of subclasses that are serialized cannot be determined at compile-time and must be explicitly registered at runtime. ### Registered subclasses Let us start with the code from the [Designing serializable hierarchy](#designing-serializable-hierarchy) section. To make it work with serialization without making it `sealed`, we have to define a [SerializersModule] using the [SerializersModule {}][SerializersModule()] builder function. In the module the base class is specified in the [polymorphic][_polymorphic] builder and each subclass is registered with the [subclass] function. Now, a custom JSON configuration can be instantiated with this module and used for serialization. > Details on custom JSON configurations can be found in > the [JSON configuration](json.md#json-configuration) section. ```kotlin val module = SerializersModule { polymorphic(Project::class) { subclass(OwnedProject::class) } } val format = Json { serializersModule = module } @Serializable abstract class Project { abstract val name: String } @Serializable @SerialName("owned") class OwnedProject(override val name: String, val owner: String) : Project() fun main() { val data: Project = OwnedProject("kotlinx.coroutines", "kotlin") println(format.encodeToString(data)) } ``` > You can get the full code [here](../guide/example/example-poly-09.kt). This additional configuration makes our code work just as it worked with a sealed class in the [Sealed classes](#sealed-classes) section, but here subclasses can be spread arbitrarily throughout the code. ```text {"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"} ``` >Please note that this example works only on JVM because of `serializer` function restrictions. >For JS and Native, explicit serializer should be used: `format.encodeToString(PolymorphicSerializer(Project::class), data)` >You can keep track of this issue [here](https://github.com/Kotlin/kotlinx.serialization/issues/1077). ### Serializing interfaces We can update the previous example and turn `Project` superclass into an interface. However, we cannot mark an interface itself as `@Serializable`. No problem. Interfaces cannot have instances by themselves. Interfaces can only be represented by instances of their derived classes. Interfaces are used in the Kotlin language to enable polymorphism, so all interfaces are considered to be implicitly serializable with the [PolymorphicSerializer] strategy. We just need to mark their implementing classes as `@Serializable` and register them. ```kotlin interface Project { val name: String } @Serializable @SerialName("owned") class OwnedProject(override val name: String, val owner: String) : Project ``` Now if we declare `data` with the type of `Project` we can simply call `format.encodeToString` as before. ```kotlin fun main() { val data: Project = OwnedProject("kotlinx.coroutines", "kotlin") println(format.encodeToString(data)) } ``` > You can get the full code [here](../guide/example/example-poly-10.kt). ```text {"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"} ``` > Note: On Kotlin/Native, you should use `format.encodeToString(PolymorphicSerializer(Project::class), data))` instead due to limited reflection capabilities. ### Property of an interface type Continuing the previous example, let us see what happens if we use `Project` interface as a property in some other serializable class. Interfaces are implicitly polymorphic, so we can just declare a property of an interface type. ```kotlin @Serializable class Data(val project: Project) // Project is an interface fun main() { val data = Data(OwnedProject("kotlinx.coroutines", "kotlin")) println(format.encodeToString(data)) } ``` > You can get the full code [here](../guide/example/example-poly-11.kt). As long as we've registered the actual subtype of the interface that is being serialized in the [SerializersModule] of our `format`, we get it working at runtime. ```text {"project":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}} ``` ### Static parent type lookup for polymorphism During serialization of a polymorphic class the root type of the polymorphic hierarchy (`Project` in our example) is determined statically. Let us take the example with the serializable `abstract class Project`, but change the `main` function to declare `data` as having a type of `Any`: ```kotlin fun main() { val data: Any = OwnedProject("kotlinx.coroutines", "kotlin") println(format.encodeToString(data)) } ``` > You can get the full code [here](../guide/example/example-poly-12.kt). We get the exception. ```text Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found. Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied. ``` We have to register classes for polymorphic serialization with respect for the corresponding static type we use in the source code. First of all, we change our module to register a subclass of `Any`: ```kotlin val module = SerializersModule { polymorphic(Any::class) { subclass(OwnedProject::class) } } ``` Then we can try to serialize the variable of type `Any`: ```kotlin fun main() { val data: Any = OwnedProject("kotlinx.coroutines", "kotlin") println(format.encodeToString(data)) } ``` > You can get the full code [here](../guide/example/example-poly-13.kt). However, `Any` is a class and it is not serializable: ```text Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found. Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied. ``` We must to explicitly pass an instance of [PolymorphicSerializer] for the base class `Any` as the first parameter to the [encodeToString][Json.encodeToString] function. ```kotlin fun main() { val data: Any = OwnedProject("kotlinx.coroutines", "kotlin") println(format.encodeToString(PolymorphicSerializer(Any::class), data)) } ``` > You can get the full code [here](../guide/example/example-poly-14.kt). With the explicit serializer it works as before. ```text {"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"} ``` ### Explicitly marking polymorphic class properties The property of an interface type is implicitly considered polymorphic, since interfaces are all about runtime polymorphism. However, Kotlin Serialization does not compile a serializable class with a property of a non-serializable class type. If we have a property of `Any` class or other non-serializable class, then we must explicitly provide its serialization strategy via the [`@Serializable`][Serializable] annotation as we saw in the [Specifying serializer on a property](serializers.md#specifying-serializer-on-a-property) section. To specify a polymorphic serialization strategy of a property, the special-purpose [`@Polymorphic`][Polymorphic] annotation is used. ```kotlin @Serializable class Data( @Polymorphic // the code does not compile without it val project: Any ) fun main() { val data = Data(OwnedProject("kotlinx.coroutines", "kotlin")) println(format.encodeToString(data)) } ``` > You can get the full code [here](../guide/example/example-poly-15.kt). ### Registering multiple superclasses When the same class gets serialized as a value of properties with different compile-time type from the list of its superclasses, we must register it in the [SerializersModule] for each of its superclasses separately. It is convenient to extract registration of all the subclasses into a separate function and use it for each superclass. You can use the following template to write it. ```kotlin val module = SerializersModule { fun PolymorphicModuleBuilder.registerProjectSubclasses() { subclass(OwnedProject::class) } polymorphic(Any::class) { registerProjectSubclasses() } polymorphic(Project::class) { registerProjectSubclasses() } } ``` > You can get the full code [here](../guide/example/example-poly-16.kt). ### Polymorphism and generic classes Generic subtypes for a serializable class require a special handling. Consider the following hierarchy. ```kotlin @Serializable abstract class Response @Serializable @SerialName("OkResponse") data class OkResponse(val data: T) : Response() ``` Kotlin Serialization does not have a builtin strategy to represent the actually provided argument type for the type parameter `T` when serializing a property of the polymorphic type `OkResponse`. We have to provide this strategy explicitly when defining the serializers module for the `Response`. In the below example we use `OkResponse.serializer(...)` to retrieve the [Plugin-generated generic serializer](serializers.md#plugin-generated-generic-serializer) of the `OkResponse` class and instantiate it with the [PolymorphicSerializer] instance with `Any` class as its base. This way, we can serialize an instance of `OkResponse` with any `data` property that was polymorphically registered as a subtype of `Any`. ```kotlin val responseModule = SerializersModule { polymorphic(Response::class) { subclass(OkResponse.serializer(PolymorphicSerializer(Any::class))) } } ``` ### Merging library serializers modules When the application grows in size and splits into source code modules, it may become inconvenient to store all class hierarchies in one serializers module. Let us add a library with the `Project` hierarchy to the code from the previous section. ```kotlin val projectModule = SerializersModule { fun PolymorphicModuleBuilder.registerProjectSubclasses() { subclass(OwnedProject::class) } polymorphic(Any::class) { registerProjectSubclasses() } polymorphic(Project::class) { registerProjectSubclasses() } } ``` We can compose those two modules together using the [plus] operator to merge them, so that we can use them both in the same [Json] format instance. > You can also use the [include][SerializersModuleBuilder.include] function > in the [SerializersModule {}][SerializersModule()] DSL. ```kotlin val format = Json { serializersModule = projectModule + responseModule } ```` Now classes from both hierarchies can be serialized together and deserialized together. ```kotlin fun main() { // both Response and Project are abstract and their concrete subtypes are being serialized val data: Response = OkResponse(OwnedProject("kotlinx.serialization", "kotlin")) val string = format.encodeToString(data) println(string) println(format.decodeFromString>(string)) } ``` > You can get the full code [here](../guide/example/example-poly-17.kt). The JSON that is being produced is deeply polymorphic. ```text {"type":"OkResponse","data":{"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"}} OkResponse(data=OwnedProject(name=kotlinx.serialization, owner=kotlin)) ``` If you're writing a library or shared module with an abstract class and some implementations of it, you can expose your own serializers module for your clients to use so that a client can combine your module with their modules. ### Default polymorphic type handler for deserialization What happens when we deserialize a subclass that was not registered? ```kotlin fun main() { println(format.decodeFromString(""" {"type":"unknown","name":"example"} """)) } ``` > You can get the full code [here](../guide/example/example-poly-18.kt). We get the following exception. ```text Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 0: Serializer for subclass 'unknown' is not found in the polymorphic scope of 'Project' at path: $ Check if class with serial name 'unknown' exists and serializer is registered in a corresponding SerializersModule. ``` When reading a flexible input we might want to provide some default behavior in this case. For example, we can have a `BasicProject` subtype to represent all kinds of unknown `Project` subtypes. ```kotlin @Serializable abstract class Project { abstract val name: String } @Serializable data class BasicProject(override val name: String, val type: String): Project() @Serializable @SerialName("OwnedProject") data class OwnedProject(override val name: String, val owner: String) : Project() ``` We register a default deserializer handler using the [`defaultDeserializer`][PolymorphicModuleBuilder.defaultDeserializer] function in the [`polymorphic { ... }`][PolymorphicModuleBuilder] DSL that defines a strategy which maps the `type` string from the input to the [deserialization strategy][DeserializationStrategy]. In the below example we don't use the type, but always return the [Plugin-generated serializer](serializers.md#plugin-generated-serializer) of the `BasicProject` class. ```kotlin val module = SerializersModule { polymorphic(Project::class) { subclass(OwnedProject::class) defaultDeserializer { BasicProject.serializer() } } } ``` Using this module we can now deserialize both instances of the registered `OwnedProject` and any unregistered one. ```kotlin val format = Json { serializersModule = module } fun main() { println(format.decodeFromString>(""" [ {"type":"unknown","name":"example"}, {"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"} ] """)) } ``` > You can get the full code [here](../guide/example/example-poly-19.kt). Notice, how `BasicProject` had also captured the specified type key in its `type` property. ```text [BasicProject(name=example, type=unknown), OwnedProject(name=kotlinx.serialization, owner=kotlin)] ``` We used a plugin-generated serializer as a default serializer, implying that the structure of the "unknown" data is known in advance. In a real-world API it's rarely the case. For that purpose a custom, less-structured serializer is needed. You will see the example of such serializer in the future section on [Maintaining custom JSON attributes](json.md#maintaining-custom-json-attributes). ### Default polymorphic type handler for serialization Sometimes you need to dynamically choose which serializer to use for a polymorphic type based on the instance, for example if you don't have access to the full type hierarchy, or if it changes a lot. For this situation, you can register a default serializer. ```kotlin interface Animal { } interface Cat : Animal { val catType: String } interface Dog : Animal { val dogType: String } private class CatImpl : Cat { override val catType: String = "Tabby" } private class DogImpl : Dog { override val dogType: String = "Husky" } object AnimalProvider { fun createCat(): Cat = CatImpl() fun createDog(): Dog = DogImpl() } ``` We register a default serializer handler using the [`polymorphicDefaultSerializer`][SerializersModuleBuilder.polymorphicDefaultSerializer] function in the [`SerializersModule { ... }`][SerializersModuleBuilder] DSL that defines a strategy which takes an instance of the base class and provides a [serialization strategy][SerializationStrategy]. In the below example we use a `when` block to check the type of the instance, without ever having to refer to the private implementation classes. ```kotlin val module = SerializersModule { polymorphicDefaultSerializer(Animal::class) { instance -> @Suppress("UNCHECKED_CAST") when (instance) { is Cat -> CatSerializer as SerializationStrategy is Dog -> DogSerializer as SerializationStrategy else -> null } } } object CatSerializer : SerializationStrategy { override val descriptor = buildClassSerialDescriptor("Cat") { element("catType") } override fun serialize(encoder: Encoder, value: Cat) { encoder.encodeStructure(descriptor) { encodeStringElement(descriptor, 0, value.catType) } } } object DogSerializer : SerializationStrategy { override val descriptor = buildClassSerialDescriptor("Dog") { element("dogType") } override fun serialize(encoder: Encoder, value: Dog) { encoder.encodeStructure(descriptor) { encodeStringElement(descriptor, 0, value.dogType) } } } ``` Using this module we can now serialize instances of `Cat` and `Dog`. ```kotlin val format = Json { serializersModule = module } fun main() { println(format.encodeToString(AnimalProvider.createCat())) } ``` > You can get the full code [here](../guide/example/example-poly-20.kt) ```text {"type":"Cat","catType":"Tabby"} ``` --- The next chapter covers [JSON features](json.md). [SerialName]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serial-name/index.html [PolymorphicSerializer]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-polymorphic-serializer/index.html [Serializable]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serializable/index.html [Polymorphic]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-polymorphic/index.html [DeserializationStrategy]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-deserialization-strategy/index.html [SerializationStrategy]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serialization-strategy/index.html [SerializersModule]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.modules/-serializers-module/index.html [SerializersModule()]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.modules/-serializers-module.html [_polymorphic]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.modules/polymorphic.html [subclass]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.modules/subclass.html [plus]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.modules/plus.html [SerializersModuleBuilder.include]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.modules/-serializers-module-builder/include.html [PolymorphicModuleBuilder.defaultDeserializer]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.modules/-polymorphic-module-builder/default-deserializer.html [PolymorphicModuleBuilder]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.modules/-polymorphic-module-builder/index.html [SerializersModuleBuilder.polymorphicDefaultSerializer]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.modules/-serializers-module-builder/polymorphic-default-serializer.html [SerializersModuleBuilder]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.modules/-serializers-module-builder/index.html [Json.encodeToString]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json/encode-to-string.html [Json]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json/index.html