zt
11/24/2022, 4:41 AM@Serializable
sealed interface Test {
@Serializable
@SerialName("a")
class A : Test
@Serializable
@SerialName("b")
class B : Test
@Serializable
@SerialName("c")
class C : Test
}
@Serializable
class Container(val contents: List<Test>)
with such json data:
{
"contents": [
{
"a": { ... }
},
{
"b": { ... }
},
{
"c": { ... }
}
]
}
Elements of contents
will always contain a single key, with an object as its value
A temporary solution to this was just to use nullable fields with the matching object names, but its not ideal.jw
11/24/2022, 4:43 AMjw
11/24/2022, 4:44 AMcontents
to be an object with curly braces or the elements of the array need to be valid JSON values.zt
11/24/2022, 4:48 AMzt
11/24/2022, 4:49 AMjw
11/24/2022, 4:55 AMList<Test>
which actual deserializes the array as a List<SomeOtherType>
where SomeOtherType
has the nullable properties a
, b
, and c
. Then, perform a map on the list where you pull out the single non-null value and return that from the serializer.zt
11/24/2022, 6:04 AMNicolas Patin
11/24/2022, 7:54 AMdata class SomeOtherType(
val a: Test.A? = null,
val b: Test.B? = null,
val c: Test.C? = null,
)
fun List<SomeOtherType>.mapToTests(): List<Test> =
this.mapNotNull {
when {
it.a != null -> it.a
it.b != null -> it.b
it.c != null -> it.c
else -> null
}
}
Nicolas Patin
11/24/2022, 7:57 AMzt
11/24/2022, 5:51 PMephemient
11/24/2022, 7:49 PMclass SingletonMapPolymorphicSerializer<T : Any>(
tSerializer: KSerializer<T>,
private val discriminator: String = tSerializer.descriptor.annotations.firstNotNullOfOrNull { it as? JsonClassDiscriminator }?.discriminator ?: "type",
) : JsonTransformingSerializer<T>(tSerializer) {
override fun transformDeserialize(element: JsonElement): JsonElement = buildJsonObject {
val (type, value) = element.jsonObject.entries.single()
put(discriminator, type)
value.jsonObject.forEach { (k, v) -> put(k, v) }
}
override fun transformSerialize(element: JsonElement): JsonElement = buildJsonObject {
putJsonObject(element.jsonObject.getValue(discriminator).jsonPrimitive.content) {
element.jsonObject.forEach { (k, v) -> if (k != discriminator) put(k, v) }
}
}
}
@Serializable
sealed interface Test {
@Serializable
@SerialName("a")
data class A(val foo: String) : Test
@Serializable
@SerialName("b")
data class B(val bar: String) : Test
}
object TestSerializer : KSerializer<Test> by SingletonMapPolymorphicSerializer(Test.serializer())
@Serializable
data class Container(
val contents: List<@Serializable(with = TestSerializer::class) Test>
)
Json.decodeFromString<Container>("""{"contents":[{"a":{...}},{"b":{...}}]}""")
zt
11/24/2022, 7:57 PMephemient
11/24/2022, 7:58 PM@Serializable(with = )
needs to be an object
for types with no type parameters, and a class
with constructor taking the KSerializer
of each type parameter for types with type parametersephemient
11/24/2022, 7:59 PMopen class SingletonMapPolymorphicSerializer<T> { ... }; object TestSerializer : SingletonMapPolymorphicSerializer(...)
instead of delegationephemient
11/24/2022, 8:00 PMSingletonMapPolymorphicSerializer
doesn't need to know anything specific about your type, just how to map between how the Json serializer wants to represent polymorphism and how you want to represent polymorphismephemient
11/24/2022, 8:00 PMuseArrayPolymorphism
you'll need a different implementationzt
11/25/2022, 4:13 AMzt
11/25/2022, 4:16 AMzt
11/25/2022, 4:21 AMList<@Serializable(with = TestSerializer::class) Test>
But this doesn't and throws the following error
@Serializable(with = TestSerializer::class)
sealed interface Test {
Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'kotlinx.serialization.descriptors.SerialDescriptor kotlinx.serialization.KSerializer.getDescriptor()' on a null object reference
Dominaezzz
11/25/2022, 12:10 PMpublic object TestSerializer : KSerializer<Test> {
private val subtypes = listOf(Test.A::class, Test.B::class, Test.C::class)
private val subclasses = listOf(Test.A.serializer(), Test.B.serializer(), Test.C.serializer())
override val descriptor: SerialDescriptor by lazy(LazyThreadSafetyMode.PUBLICATION) {
buildClassSerialDescriptor("packagename.Test") {
for (subclass in subclasses) {
element(subclass.descriptor.serialName, subclass.descriptor, isOptional = true)
}
}
}
override fun serialize(encoder: Encoder, value: Test) {
val index = subtypes.indexOf(value::class)
@Suppress("UNCHECKED_CAST")
val subclass = subclasses[index] as KSerializer<Test>
encoder.encodeStructure(descriptor) {
encodeSerializableElement(descriptor, index, subclass, value)
}
}
override fun deserialize(decoder: Decoder): Test {
return decoder.decodeStructure(descriptor) {
val index = decodeElementIndex(descriptor)
check(index != CompositeDecoder.DECODE_DONE) {
"No field in json object"
}
val subclass = subclasses[index]
decodeSerializableElement(descriptor, index, subclass)
}
}
}
ephemient
11/25/2022, 8:47 PMbecause the@Serializable(with = TestSerializer::class)
SingletonMapPolymorphicSerializer
I gave you, depends on the original Test.serializer()
, which you've prevented from being generatedephemient
11/25/2022, 8:49 PMsealed
hierarchies, and you can screw yourself over by missing a subclassDominaezzz
11/25/2022, 8:57 PMwhen
statement in the serialiser but I do agree that it's not ideal to bypass the code gen. It's a shame that the current state of polymorphic serialisation is rather opinionated.ephemient
11/25/2022, 11:42 PM@Serializable(with = TestSerializer::class)
sealed interface Test {
@Serializable
class A : Test
@Serializable
class B : Test
@Serializable
class C : Test
}
@Serializable
class TestSurrogate(
val a: Test.A? = null,
val b: Test.B? = null,
val c: Test.C? = null,
)
object TestSerializer : KSerializer<Test> {
override val descriptor: SerialDescriptor = SerialDescriptor("my.package.name.Test", TestSurrogate.serializer().descriptor)
override fun serialize(encoder: Encoder, value: Test) {
encoder.encodeSerializableValue(
TestSurrogate.serializer(),
when (value) {
is Test.A -> TestSurrogate(a = value)
is Test.B -> TestSurrogate(b = value)
is Test.C -> TestSurrogate(c = value)
}
)
}
override fun deserialize(decoder: Decoder): Test {
val value = decoder.decodeSerializableValue(TestSurrogate.serializer())
return value.a ?: value.b ?: value.c ?: throw SerializationException()
}
}
zt
11/26/2022, 12:37 AMzt
11/26/2022, 2:19 AMserializersModule = SerializersModule {
polymorphic(Any::class) {
default { null }
}
}
ephemient
11/26/2022, 3:03 AMdata class TypedSerializer<T : Any>(val kclass: KClass<T>, val serializer: KSerializer<T>)
inline fun <reified T: Any> TypedSerializer(serializer: KSerializer<T>): TypedSerializer<T> = TypedSerializer(T::class, serializer)
class PolymorphicOrNullSerializer<T : Any>(serialName: String, vararg types: TypedSerializer<out T>) : KSerializer<T?> {
private val types = types
override val descriptor: SerialDescriptor = buildClassSerialDescriptor(serialName) {
for ((kclass, serializer) in types) element(serializer.descriptor.serialName, serializer.descriptor, isOptional = true)
}
override fun serialize(encoder: Encoder, value: T?) {
encoder.encodeStructure(descriptor) {
types.forEachIndexed { index, (kclass, serializer) ->
if (kclass.isInstance(value)) {
encodeSerializableElement(descriptor, index, serializer as KSerializer<T>, value as T)
return@encodeStructure
}
}
}
}
override fun deserialize(decoder: Decoder): T? = decoder.decodeStructure(descriptor) {
when (val index = decodeElementIndex(descriptor)) {
CompositeDecoder.DECODE_DONE -> null
CompositeDecoder.UNKNOWN_NAME -> null
else -> decodeSerializableElement(descriptor, index, types[index].serializer)
}
}
}
@Serializable
data class Container(
val contents: List<@Serializable(with = TestSerializer::class) Test?>,
)
object TestSerializer : KSerializer<Test?> by PolymorphicOrNullSerializer(
"my.package.name.Test",
TypedSerializer(Test.A.serializer()),
TypedSerializer(Test.B.serializer()),
TypedSerializer(Test.C.serializer()),
)
Json { ignoreUnknownKeys = true }.decodeFromString<Container>("""{"contents":[{"a":...},{"z":...}]}""")
zt
11/26/2022, 4:50 AMephemient
11/26/2022, 4:58 AMTest::class.sealedSubclasses.map { it.serializer() }
but this depends on kotlin-reflect, doesn't take sub-subclasses into account (can be fixed), will be slow or outright fail on Android, and doesn't provide any compile-time feedback if you mistakenly miss a @Serializable
annotationephemient
11/26/2022, 5:00 AMKClass<T>.serializer()
is @InternalSerializationApi
zt
11/26/2022, 5:01 AMzt
02/23/2023, 4:50 AM{
"list": [
{
"x": {
"a": true
}
},
{
"y": {
"b": false
}
}
]
}
to
{
"list": [
{
"type": "x",
"a": true
},
{
"type": "y",
"b": true
}
]
}
would that be simpler?