I have a closed polymorphic structure like this (o...
# serialization
r
I have a closed polymorphic structure like this (only one subclass shown for simplicity):
Copy code
@Serializable
sealed class Foo<out T> {
  @Serializable
  data class Bar<out T>(
    val value: T?,
  ): Foo<T>()
}
Json.encodeToString(Foo.Bar(true))
works, but if it is wrapped inside another Serializable e.g.:
Copy code
@Serializable
data class Whatever(val foo: Foo<Boolean>)
Json.encodeToString(Whatever(Foo.Bar(true)))
produces the error:
Copy code
Exception in thread "main" kotlinx.serialization.SerializationException: Class 'Boolean' is not registered for polymorphic serialization in the scope of 'Any'.
Mark the base class as 'sealed' or register the serializer explicitly.
d
Iirc there is a PR for this
r
Have a reference?
I've done some poking around but can't find anything
r
That's a similar case but has a completely different failure mode
The specific case given in the test works just fine, so its not the same issue at all
d
Ah I see, have you tried registering a contextual serialiser for the subclass?
r
I have but either its not working or I haven't got the syntax right
This is the work-around I expected to work, but the contextual provider is never even called:
Copy code
@Serializable
data class Whatever(@Contextual val foo: Foo<Boolean>)

val format = Json { SerializersModule {
  contextual(Foo::class) { args ->
    println("args: $args")
    // just testing, we know the type arg is Boolean for now
    Boolean.serializer()
  }
} }
I also tried:
Copy code
val format = Json { SerializersModule {
    polymorphic(Any::class) {
      subclass(Boolean::class, Boolean.serializer())
    }
  } }
but this doesn't work either.
d
Oh not for Foo do it for Bar.
r
You mean like this?
Copy code
@Serializable
sealed class Foo<out T> {
  @Serializable
  data class Bar<out T>(
    @Contextual val value: T?,
  ): Foo<T>()
}

  val format = Json { SerializersModule {
    contextual(Foo.Bar::class) { args ->
      println("args: $args")
      Boolean.serializer()
    }
  } }
If so, no, I tried that and it doesn't work either.
d
Thinking about how polymorphism currently works, what you want to do simply isn't possible.
Create an issue I think.
r
That sucks. What is the best work-around in your opinion?
d
Not sure, do you really need the generics?
r
Only way I can avoid them is by a lot of code duplication.
d
Do you only serialise Boolean?
r
No. I have 3 subclasses of
Foo
, and I serialize a bunch of different types, one of which is Boolean, but could also be other primitives or data classes. Without generics I'd have to create 3 subclasses per type that is being serialized. Unless I'm missing something.
d
Yeah that's what I was going to suggest.
Well, since it's a sealed class you could just write a custom serialiser.
r
That sounds promising. Let me read the docs on that.
Wait, what do I need the custom serializer for? Serde already works when used directly on the sealed class. So, do I need to write a custom serializer for the wrapper?
d
Oh, it works directly? That's odd. I'm not sure then.
r
Yup, that's what is so weird about this. This works completely perfectly:
Copy code
Json.encodeToString(Foo.Bar(true))
but:
Copy code
Json.encodeToString(Whatever(Foo.Bar(true)))
does not.
Well, a workaround appears to be to create a
ValueHolder
around the type T, so that T is bound by another sealed class hierarchy instead of just being
Any
.
Copy code
@Serializable
sealed class ValueHolder<T> {
  abstract val value: T?
}

@Serializable
data class BooleanValueHolder(override val value: Boolean?): ValueHolder<Boolean>()

@Serializable
sealed class Foo<T> {
  @Serializable
  data class Bar<T>(
    val value: ValueHolder<T>,
  ): Foo<T>()
}
Then serializing
data class Whatever(val foo: Foo<Boolean>)
works as expected.