BenWoodworth / knbt

Kotlin NBT library for kotlinx.serialization
GNU Lesser General Public License v3.0
72 stars 2 forks source link

ClassCastException when serializing sealed classes #12

Closed AlexCouch closed 1 year ago

AlexCouch commented 2 years ago

I have found a problem regarding the poymorphic serialization in the default serializer. When encoding a serializable marked sealed class, it throws an exception with the following stack trace:

Exception in thread "main" java.lang.ClassCastException: class kotlinx.serialization.descriptors.PolymorphicKind$SEALED cannot be cast to class kotlinx.serialization.descriptors.StructureKind (kotlinx.serialization.descriptors.PolymorphicKind$SEALED and kotlinx.serialization.descriptors.StructureKind are in unnamed module of loader 'app')
    at net.benwoodworth.knbt.internal.DefaultNbtEncoder.encodeElement(DefaultNbtEncoder.kt:27)
    at kotlinx.serialization.encoding.AbstractEncoder.encodeStringElement(AbstractEncoder.kt:65)
    at kotlinx.serialization.internal.AbstractPolymorphicSerializer.serialize(AbstractPolymorphicSerializer.kt:34)
    at kotlinx.serialization.encoding.Encoder$DefaultImpls.encodeSerializableValue(Encoding.kt:282)
    at kotlinx.serialization.encoding.AbstractEncoder.encodeSerializableValue(AbstractEncoder.kt:18)
    at net.benwoodworth.knbt.AbstractNbtEncoder.encodeSerializableValue(NbtEncoder.kt:100)
    at kotlinx.serialization.encoding.AbstractEncoder.encodeSerializableElement(AbstractEncoder.kt:80)
    at Person$Child.write$Self(NbtTest.kt:16)
    at Person$Child$$serializer.serialize(NbtTest.kt:16)
    at Person$Child$$serializer.serialize(NbtTest.kt:16)
    at kotlinx.serialization.encoding.Encoder$DefaultImpls.encodeSerializableValue(Encoding.kt:282)
    at kotlinx.serialization.encoding.AbstractEncoder.encodeSerializableValue(AbstractEncoder.kt:18)
    at net.benwoodworth.knbt.AbstractNbtEncoder.encodeSerializableValue(NbtEncoder.kt:100)
    at kotlinx.serialization.encoding.AbstractEncoder.encodeSerializableElement(AbstractEncoder.kt:80)
    at net.benwoodworth.knbt.internal.RootClassSerializer.serialize(RootClassSerializer.kt:23)
    at kotlinx.serialization.encoding.Encoder$DefaultImpls.encodeSerializableValue(Encoding.kt:282)
    at kotlinx.serialization.encoding.AbstractEncoder.encodeSerializableValue(AbstractEncoder.kt:18)
    at net.benwoodworth.knbt.AbstractNbtEncoder.encodeSerializableValue(NbtEncoder.kt:100)
    at net.benwoodworth.knbt.NbtFormatKt.encodeToNbtWriter(NbtFormat.kt:49)
    at net.benwoodworth.knbt.Nbt.encodeToSink(Nbt.kt:25)
    at net.benwoodworth.knbt.JvmStreamsKt.encodeToStream(JvmStreams.kt:18)
    at NbtTestKt.main(NbtTest.kt:46)
    at NbtTestKt.main(NbtTest.kt)

When looking at the line that threw the exception, it shows this:

when (descriptor.kind as StructureKind) {
    //...
}

The reason why it throws the error is because descriptor.kind is of type SerialKind, which is in fact a supertype of StructureKind, however, the type of descriptor.kind at this point is PolymorphicKind, which is not a subtype of StructureKind. They are on different parts of the hierarchy.

SerialKind | - PolymorphicKind | - ... |- StructureKind |- ...

My Nbt object setup looks like this:

val nbt = Nbt {
        variant = NbtVariant.Java
        compression = NbtCompression.None
        compressionLevel = null
        encodeDefaults = false
        ignoreUnknownKeys = false
        serializersModule = SerializersModule {
            polymorphic(DataElement::class){
                subclass(DataElement.Section::class)
                subclass(DataElement.Group::class)
                subclass(DataElement.Unit::class)
            }
        }
    }

However, this also crashes if I use the EmptySerializersModule.

I was able to reproduce it with the following code:

import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.modules.EmptySerializersModule
import net.benwoodworth.knbt.*
import okio.use
import java.io.File

@kotlinx.serialization.Serializable
sealed class Person{
    abstract val name: String
    abstract val age: Int

    @kotlinx.serialization.Serializable
    class Adult(override val name: String, override val age: Int, val occupancy: String): Person()
    @kotlinx.serialization.Serializable
    class Teen(override val name: String, override val age: Int, val school: String): Person()
    @kotlinx.serialization.Serializable
    class Child(override val name: String, override val age: Int, val father: Person, val mother: Person): Person()
}

fun main(){
    val nbt = Nbt {
        variant = NbtVariant.Java // Java, Bedrock, BedrockNetwork
        compression = NbtCompression.None // None, Gzip, Zlib
        compressionLevel = null // in 0..9
        encodeDefaults = false
        ignoreUnknownKeys = false
        serializersModule = EmptySerializersModule
    }
    val father = Person.Adult("jon", 32, "accountant")
    val mother = Person.Adult("kathy", 31, "baker")
    val child = Person.Child("nate", 6, father, mother)
    val fs = File("nbt.test")
    val childBA = nbt.encodeToByteArray(child)
//    fs.outputStream().use {
//        nbt.encodeToStream(child, it)
//    }
//    val childFromNbt: Person.Child = fs.inputStream().use {
//        nbt.decodeFromStream(it)
//    }
    val childFromNbt: Person.Child = nbt.decodeFromByteArray(childBA)
    println(childFromNbt)

The okio code which is commented was the original code I used to reproduce it but I decided tried it for just a regular bytearray encode/decode call. Of course it would be same because this crash is in the default nbt encoder.

BenWoodworth commented 2 years ago

Hey! This might just be the best, most detailed bug report I've ever gotten. Thanks for that! :P

Started poking around a little bit and I think the issue stems from here https://github.com/BenWoodworth/knbt/blob/70fa0864bca7684c6853844eb7bb61f425b5ebf7/src/commonMain/kotlin/NbtEncoder.kt#L91-L101

When I wrote this I didn't have my head fully wrapped around kotlinx.serialization's polymorphic serialization so didn't support it. And I thought this code would catch any polymorphism and fail with a nice friendly polite error... but (and I'm just now finding this out) there's a problem

Before we get to your error, execution gets to this point with serializer being a SealedClassSerializer<Person>, which extends AbstractPolymorphicSerializer, but does not implement PolymorphicSerializer. So it passes up that "not yet supported" case and gracefully delegates to the super class on line 100.

So that means my assumption here that descriptor.kind is a StructureKind fails: https://github.com/BenWoodworth/knbt/blob/70fa0864bca7684c6853844eb7bb61f425b5ebf7/src/commonMain/kotlin/internal/DefaultNbtEncoder.kt#L26-L39

BenWoodworth commented 2 years ago

I pushed this commit to fix the polymorphic check: https://github.com/BenWoodworth/knbt/commit/be3621d3196ae3235dec105427fd6e0e969ba799

And with that your example now works, if you change father and mother to be Adults.

Other polymorphic serialization will still fail though since I haven't implemented support yet. I opened #13 so if you want support for that please toss your use cases (and perhaps test cases) so I have something to work towards :)

I'll post a new release when I get a chance, but until then knbt:0.11.0-SNAPSHOT should have the changes I pushed soon!