Skip to content

Support nested polymorphic hierarchies with different discriminators #2934

Open
@TheKvist

Description

@TheKvist

TL;DR:

I want to build a library to deserialize JSON like this:

[
  {
    "cmsType": "person",
    "firstName": "John",
    "lastName": "Doe"
  },
  {
    "cmsType": "product",
    "name": "Some product",
    "price": 100
  },
  {
    "cmsType": "link",
    "linkType": "internal",
    "reference": "some-reference"
  },
  {
    "cmsType": "link",
    "linkType": "external",
    "url": "https://example.com"
  }
]

into a hierarchy of classes somewhat like this:

@Serializable
@JsonClassDiscriminator("cmsType")
abstract class CmsObject

@Serializable
@SerialName("person")
class Person(val firstName: String, val lastName: String) : CmsBase()

@Serializable
@SerialName("product")
class Product(val name: String, val price: Int) : CmsBase()

@Serializable
@SerialName("link")
@JsonClassDiscriminator("linkType")
abstract class Link : CmsBase()

@Serializable
@SerialName("internal")
class InternalLink(val reference: String) : Link()

@Serializable
@SerialName("external")
class ExternalLink(val url: String) : Link()

where everything except CmsObject is user-defined and registered at runtime.


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 content types are user-defined and unknown to the library. They may also be polymorphic through a different discriminator property. They could also contain arrays of any combination of CMS types, where I'm hitting the limits of what seems doable with kotlinx.serialization.

To illustrate, here's an abstract representation of some basic JSON that the CMS might return and that library should be able to deserialize.

[
  {
    "cmsType": "person",
    "firstName": "John",
    "lastName": "Doe"
  },
  {
    "cmsType": "product",
    "name": "Some product",
    "price": 100
  }
]

The library only supplies a base class, setting up the cmsType.

@Serializable
@JsonClassDiscriminator("cmsType")
abstract class CmsObject

Users then extend this base class with their types and register those types with the library, which works for direct subclasses of CmsObject.

@Serializable
@SerialName("person")
class Person(val firstName: String, val lastName: String) : CmsBase()

@Serializable
@SerialName("product")
class Product(val name: String, val price: Int) : CmsBase()

Since the hierarchy can't be sealed, the library will tie everything together with a SerializersModule.

serializersModule = SerializersModule {
    polymorphic(CmsBase::class) {
        subclass(Person::class)
        subclass(Product::class)
    }
}

This works fine for the simple case. The first problem arises when the user introduces a new polymorphic hierarchy with its own class discriminator. For example, the CMS might return JSON like this:

[
  {
    "cmsType": "link",
    "linkType": "internal",
    "reference": "some-reference"
  },
  {
    "cmsType": "link",
    "linkType": "external",
    "url": "https://example.com"
  }
]

In order to represent this correctly, the user would need to define a new base class for the link type and subclasses for the internal and external types. In order to discriminate between the subclasses based on the linkType property, the user would have to use the @JsonClassDiscriminator annotation.

@Serializable
@SerialName("link")
@JsonClassDiscriminator("linkType")
abstract class Link : CmsBase()

@Serializable
@SerialName("internal")
class InternalLink(val reference: String) : Link()

@Serializable
@SerialName("external")
class ExternalLink(val url: String) : Link()

And here's where I'm hitting the first wall: The @JsonClassDiscriminator annotation can not be used to "add" another discriminator to the hierarchy.

One way to get around that could be to have the user define and supply a JsonContentPolymorphicSerializer for their Link class where they do the discrimination manually, but I would rather not force that on them.

Also, this doesn't save me from the fact that the above class hierarchy can not be registered in the SerializersModule, because it would have to look somewhat like this:

serializersModule = SerializersModule {
    polymorphic(CmsBase::class) {
        subclass(Person::class)
        subclass(Product::class)
        subclass(Link::class) // This is not possible
    }
    polymorphic(Link::class) {
        subclass(InternalLink::class)
        subclass(ExternalLink::class)
    }
}

Here, we're registering Link as a subclass of CmsBase so that it's serializer is selected for the link type, and then we're registering InternalLink and ExternalLink as subclasses of Link, but because Link is an abstract class, it can not be registered as a subclass of CmsBase in the SerializersModule.

This is total roadblock for me. The only way I can think of to get around this is to use a custom serializer for the entire CmsObject hierarchy and do the discrimination manually, but before I go down that rabbit hole, I wanted to ask for ideas and suggestions on how to solve this problem.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions