Skip to content

feat: adding new page for polymorphism#3141

Open
daniCsorbaJB wants to merge 6 commits intodoc-restructuring-masterfrom
doc-restruturing-update-8
Open

feat: adding new page for polymorphism#3141
daniCsorbaJB wants to merge 6 commits intodoc-restructuring-masterfrom
doc-restruturing-update-8

Conversation

@daniCsorbaJB
Copy link
Copy Markdown

This PR adds the page related to Polymorphism to the rewritten Kotlin serialization docs

Related Youtrack ticket: KT-83541

Copy link
Copy Markdown
Member

@sandwwraith sandwwraith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

Copy link
Copy Markdown
Member

@sandwwraith sandwwraith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall structure looks good

Polymorphism allows objects of different types to be serialized through a common declared supertype.
This applies to class polymorphism, where subclasses share a common interface or base class.

> This section focuses on class polymorphism, where subclasses share a common interface or base class.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line duplicates the one above. I think you can remove the tip completely and join the references:

Class polymorphism allows working with objects of different types via common interface or a base class. Kotlin serialization supports polymorphism, allowing different types to be serialized through a common declared supertype. For an overview of how interfaces and inheritance work in Kotlin, see [Interfaces](interfaces.md) and [Inheritance](inheritance.md).
Kotlin serialization is static by default.
...

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I like it 👍


In this example, even though the runtime value is the `OwnedProject` subclass, only the properties of the declared `Project` static type are serialized.

Kotlin serialization supports two ways to work with polymorphic hierarchies:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need some kind of joiner to the previous sentence here. WDYT of "To serialize runtime values, you need to use one of two ways to work with polymorphic hierarchies:"?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean 🤔
I think it's a good idea to connect it with an active sentence.

Regarding the "runtime" part do you think that's necessary? 🤔 WDYT:

To serialize values in a polymorphic class hierarchy, Kotlin serialization provides two approaches:


A class hierarchy is considered to use closed polymorphism when all possible subclasses are guaranteed to be known at compile time.

To provide this guarantee, [use a `sealed class` or `interface` as the base type](#serialize-closed-polymorphic-classes).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Giving a link to literally the next line feels pointless. Maybe you wanted to link to https://kotlinlang.org/docs/sealed-classes.html instead?

Also: "sealed class or sealed interface" for more emphasis.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed to both 👍

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be worthwhile to especially highlight sealed interfaces as being able to support module-free/closed polymorphism without restricting the class hierarchy.


To provide this guarantee, [use a `sealed class` or `interface` as the base type](#serialize-closed-polymorphic-classes).

### Serialize closed polymorphic classes
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only section under "Closed polymorphism in Kotlin serialization" — are you sure sub-header is needed?

Copy link
Copy Markdown
Author

@daniCsorbaJB daniCsorbaJB Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I added it mainly for Table of Contents scannability + better metadata (what people will see when they hover over open/closed polymorphism if we link to it - tbh this we can still retain with your proposal so not much of an issue).

On the other hand, If we remove the “Closed polymorphism” and “Open polymorphism” parent sections and move “Serialize closed…” and “Serialize open…” to the second level, we introduce four additional entries in the ToC. That pushes the current fourth-level sections higher and makes the ToC longer.

On the most common screen resolution that would require scrolling to see all entries. (but tbh it might not be as important so we might not be able to avoid this anyway 😞 )

I'll test how it looks that way 👍 — also as an alternative, we could even break the doc into multiple pages.
(Polymorphism overview - Closed polymorphism - Open polymorphism)


### Serialize closed polymorphic classes

To serialize a closed polymorphic hierarchy, use a `sealed class` or a `sealed interface` as the base type and mark all subclasses with `@Serializable`:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you remove sub-header, this can be nicely joined:

To provide this guarantee, [use a `sealed class` or `interface` as the base type](#serialize-closed-polymorphic-classes). After you've done  that, mark all subclasses with `@Serializable` and you're ready to serialize a closed polymorphic hierarchy:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not also required to mark the base type as @Serializable

Interfaces and abstract classes use polymorphic serialization by default.

To serialize a property with a non-serializable type, such as `Any`,
or when the property’s base type is an open class, annotate the property with [`@Polymorphic`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-polymorphic/).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
or when the property’s base type is an open class, annotate the property with [`@Polymorphic`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-polymorphic/).
or when the property’s type is an open class, annotate the property with [`@Polymorphic`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-polymorphic/).

Properties types are fully static.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this annotation works on closed types too. It forces the use of the polymorphic serializer (and polymorphic resolution).

abstract class Response<out T>

@Serializable
@SerialName("OkResponse")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need serial name in this and all examples below?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The examples tend to be a bit much here. It would be possible if only the relevant sections were highlighted and the full code available on user action.

Copy link
Copy Markdown
Author

@daniCsorbaJB daniCsorbaJB Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need serial name in this and all examples below?

I agree we don't need it — removed it. 👍

The examples tend to be a bit much here. It would be possible if only the relevant sections were highlighted and the full code available on user action.

🤔 We could show just the most relevant part, but I'm worried people would get confused what they are merging:

val format = Json { serializersModule = projectModule + responseModule }

data class OwnedProject(override val name: String, val owner: String) : Project()
```

To handle unknown polymorphic subtypes like `BasicProject`, configure a default deserializer for the base type.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BasicProject is not unknown. It is a default choice for other types.

To handle unknown polymorphic subtypes of Project...

2. Specify a lambda in `polymorphicDefaultSerializer()` that returns a [`SerializationStrategy`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serialization-strategy/)` for the runtime value.

Let's look at an example with two private classes, `CatImpl` and `DogImpl`.
To avoid raising their visibility, register a default serializer for `Animal` that selects a serializer based on the runtime value:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"runtime value" would be of a private class in this case. So more correct wording is "selects a serializer based on the public interface:" or "selects a serializer without having to refer to the private implementation classes:"

Copy link
Copy Markdown
Author

@daniCsorbaJB daniCsorbaJB Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point & yes, I agree it shouldn't be runtime value, but doesn't it select it based on runtime type just through the public interface or am I wrong here? 🤔

so perhaps would it work with:

...that selects a serializer based on the runtime type through public interfaces.

val module = SerializersModule {
polymorphicDefaultSerializer(Animal::class) { instance ->
@Suppress("UNCHECKED_CAST")
// Determines the appropriate serializer using a when block based on the runtime value
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Copy Markdown
Contributor

@pdvrieze pdvrieze left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a look through the document. Compared to the previous ones this document gets quite deep into technical complexities.

Even the structure is a bit complex and can probably be streamlined a bit by being very diligent in overlapping/distinct cases as well as (even stronger) separation of aspects (e.g. writing the serializable types vs consumption + SerializerModules).


A class hierarchy is considered to use closed polymorphism when all possible subclasses are guaranteed to be known at compile time.

To provide this guarantee, [use a `sealed class` or `interface` as the base type](#serialize-closed-polymorphic-classes).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be worthwhile to especially highlight sealed interfaces as being able to support module-free/closed polymorphism without restricting the class hierarchy.


### Serialize closed polymorphic classes

To serialize a closed polymorphic hierarchy, use a `sealed class` or a `sealed interface` as the base type and mark all subclasses with `@Serializable`:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not also required to mark the base type as @Serializable


By default, Kotlin serialization uses the fully qualified class name as the type discriminator property value for polymorphic subclasses.

You can [customize their serial names](serialization-customization-options.md#customize-serial-names) with the [`@SerialName`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serial-name/) annotation.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may also want to be very clear on the distinction between @SerialName applied to the type (in which it specifies the type name for polymorphism - or other format specific purposes), and applied to properties where it specifies the property/element name (not the name of its type).

(in retrospect it would have been good to have a @SerialTypeName annotation as and restrict the annotation targets accordingly)


You can [customize their serial names](serialization-customization-options.md#customize-serial-names) with the [`@SerialName`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serial-name/) annotation.

> You must use a unique `@SerialName` for each serializable class in a polymorphic hierarchy. Otherwise, an `IllegalStateException` is thrown.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, type names are expected to be unique for the entire use of a format (not just individual invocations) as formats would use this for caching.

The way it is written here implies that it is fine to have conflicting names outside of polymorphism (and it generally isn't – it creates really tricky bugs)

Comment on lines +184 to +187
> You can also configure JSON to use a different key name for the class discriminator.
> For more information, see the [Specify class discriminator for polymorphism](serialization-json-configuration.md#specify-class-discriminator-for-polymorphism) section.
>
{style="tip"}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some additional clarity, you may want to mention that "type" is the default name. However, would this tip not be better placed at line 87 (where you actually discuss the "type" attribute).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both good points I agree — moved it and rewrote that part to reflect this 👍

Interfaces and abstract classes use polymorphic serialization by default.

To serialize a property with a non-serializable type, such as `Any`,
or when the property’s base type is an open class, annotate the property with [`@Polymorphic`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-polymorphic/).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this annotation works on closed types too. It forces the use of the polymorphic serializer (and polymorphic resolution).

Comment on lines +617 to +621
Kotlin serialization can't automatically determine the concrete type of a generic type parameter at runtime.
As a result, it can't pick a serializer for that parameter without explicit configuration.

To provide this configuration, register the subtype in a `SerializersModule` with an explicit serializer.
`PolymorphicSerializer(Any::class)` has the broadest scope, but you can use a more specific serializer when you know which concrete types the generic value can have.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is getting quite advanced. It needs a bit more clarity on what it can and can not do. Make clear that this is not needed for the non-polymorphic case or if the generic argument is present on the ancestor:

@Serializable
sealed interface Optional<T> {
  @Serializable
  object None: Optional<Nothing>

  @Serializable
  class Value<T>(val value: T): Optional<T>
}

abstract class Response<out T>

@Serializable
@SerialName("OkResponse")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The examples tend to be a bit much here. It would be possible if only the relevant sections were highlighted and the full code available on user action.


In this example, deserialization doesn't rely on the `type` field to differentiate subtypes and instead uses the plugin-generated serializer for `BasicProject`.
This approach assumes that unknown subtypes follow a known structure.
If unknown input varies in structure, use a [custom serializer](create-custom-serializers.md) instead.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also mention that an alternative is for the format to be configured to ignore unknown keys.

And in most cases this only really works with format specific handling in the custom serializers.

Comment on lines +857 to +858
You can serialize values of a polymorphic base type without registering every concrete subtype.
Use this when you don't have access to the full type hierarchy, or when it changes a lot.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it is worth mentioning that a custom serializer could also be used, and would be nicer from a consumer perspective and more powerful from the provider perspective.

The primary use case for the default seems to be to support forward compatibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants