Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support nested polymorphic hierarchies with different discriminators #2934

Open
TheKvist opened this issue Feb 24, 2025 · 0 comments
Open

Support nested polymorphic hierarchies with different discriminators #2934

TheKvist opened this issue Feb 24, 2025 · 0 comments

Comments

@TheKvist
Copy link

TheKvist commented Feb 24, 2025

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.

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

No branches or pull requests

1 participant