Olaf Hoffmann
02/11/2025, 8:00 AMkotlinx.serialization
when trying to achieve
multiple levels of polymorphism with individual class discriminators for each level.
I'm building a library that allows users to deserialize JSON returned from a headless CMS. The CMS supplies a class
discriminator for every type returned in its API, but the types themselves are user-defined and may be polymorphic
themselves, which is what's causing me trouble.
Here's an abstract representation of some JSON that the CMS might return and that library should be able to deserialize.
[
{
"cmsDiscriminator": "someUserType",
"someUserValue": "value"
},
{
"cmsDiscriminator": "otherUserType",
"otherUserValue": "value"
}
]
The library only supplies the base class, setting up the cmsDiscriminator
.
@Serializable
@JsonClassDiscriminator("cmsDiscriminator")
abstract class CmsBase
Users then extend this base class with their own types which works fine for direct subclasses of CmsBase
.
@Serializable
@SerialName("someUserType")
class SomeUserType(val someUserValue: String) : CmsBase()
@Serializable
@SerialName("otherUserType")
class OtherUserType(val otherUserValue: String) : CmsBase()
The problem arises when the user wants to introduce a new polymorphic hierarchy, where the base class is a subclass of
CmsBase
and has its own class discriminator, for JSON like this
[
{
"cmsDiscriminator": "polymorphicUserType",
"userDiscriminator": "somePolymorphicSubType",
"someSubValue": "value"
},
{
"cmsDiscriminator": "polymorphicUserType",
"userDiscriminator": "otherPolymorphicSubType",
"otherSubValue": "value"
}
]
The users would have to define a class hierarchy like this:
@Serializable
@SerialName("polymorphicUserType")
@JsonClassDiscriminator("userDiscriminator") // doesn't work
abstract class UserPolymorphicBase : CmsBase()
@Serializable
@SerialName("somePolymorphicSubType")
class SomePolymorphicSubType(val someSubValue: String) : UserPolymorphicBase()
@Serializable
@SerialName("otherPolymorphicSubType")
class OtherPolymorphicSubType(val otherSubValue: String) : UserPolymorphicBase()
However, here's where I'm encountering my first problem: The @JsonClassDiscriminator
annotation can't be used on
UserPolymorphicBase
, because the Argument values for inheritable serial info annotation 'JsonClassDiscriminator'
must be the same as the values in parent type 'CmsBase'
, not allowing us to change the discriminator value for the
subclasses of some class in that hierarchy.
One way to get around that would be to have the user define and supply a JsonContentPolymorphicSerializer
for their
UserPolymorphicBase
class, but I would rather not force that on them. Additionally, it does not save me from the
second problem, which is that the library has to define a custom SerializerModule
since the CmsBase
class is not sealed
.
In the library, I would have to do something like this
serializersModule = SerializersModule {
polymorphic(CmsBase::class) {
subclass(SomeUserType::class)
subclass(OtherUserType::class)
subclass(UserPolymorphicBase::class) // doesn't work
}
polymorphic(UserPolymorphicBase::class) {
subclass(SomePolymorphicSubType::class)
subclass(OtherPolymorphicSubType::class)
}
}
But that fails because UserPolymorphicBase
is not a concrete class and thus can't be registered as a polymorphic subclass of CmsBase
.
This issue gets worse when considering the fact that the polymorphic hierarchy can be nested arbitrarily deep as far as
the user is concerned and should accordingly be supported by the library.
I believe this should be possible to do with a lot of workarounds, but I was hoping not to have to implement a bunch of
custom deserialization logic for this to work. Is there a way to achieve this with what we have available in
kotlinx.serialization
?
Thanks in advance for any help and suggestions!