BenWoodworth / knbt

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

How to serialize into a Json ? #20

Closed Ayfri closed 1 year ago

Ayfri commented 1 year ago

Hi, for a specific reason, I have to serialize an object as a JSON that also contains a NbtTag property, but I can't figure out how to make it works. By having the default Serializer I get the following error :

Class 'NbtList' is not registered for polymorphic serialization in the scope of 'NbtTag'.
Mark the base class as 'sealed' or register the serializer explicitly.
BenWoodworth commented 1 year ago

Hey! I think you have the right approach, but there was a bug that made @Serializable sealed interfaces (like NbtTag) not work correctly out of the box. I'm planning on having that fixed in knbt v0.12, since serializable sealed interfaces are now supported, but it might be a bit.

I can get you a more complete answer later, but something like this might work for now. (this is from memory, so it might not be quite right)

val polymorphicNbtSerializersModule = SerializersModule {
    polymorphic(NbtTag::class, NbtTag.serializer()) {
        subclass(NbtByte::class, NbtByte.serializer())
        subclass(NbtShort::class, NbtShort.serializer())
        // ...and all the other NbtTag types
    }
}

val json = Json {
    // ...
    serializersModule = polymorphicNbtSerializersModule
}

I'll play around with it later to get something that working :)

BenWoodworth commented 1 year ago

Although on second thought I'm not sure that'll work. The NbtTag serializers require the NbtEncoder/Decoder: https://github.com/BenWoodworth/knbt/blob/d288924186a991ccfa6c0b61556d3a49a9f92fd0/src/commonMain/kotlin/NbtTagSerializers.kt#L17

Same as how the JsonElements require the JsonEncoder/Decoder:

https://github.com/Kotlin/kotlinx.serialization/blob/dc950f5098162a3f8dd742d04b95f9a0405470e1/formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt#L40-L41

https://github.com/Kotlin/kotlinx.serialization/blob/dc950f5098162a3f8dd742d04b95f9a0405470e1/formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt#L192

BenWoodworth commented 1 year ago

So you'd need a custom serializer, which I can help you with.

How do you feel about storing the NBT as a JSON string value? Writing it as SNBT is probably the easiest thing to do

Ayfri commented 1 year ago

It's complicated, because I'm serializing a data class DataPack, containing a serializable data class Pack, containing a property description which is a NbtTag (it's for generating the pack.mcmeta from datapacks). So if I serialize all this to a SNBT, it won't be a valid JSON.

BenWoodworth commented 1 year ago

Ah, gotcha! Just got off work so I'm starting to look into it.

Is this the one you're talking about? And changing it from String to NbtTag https://github.com/Ayfri/Datapack-DSL/blob/e57be5bb68330216958d23034730ff5616704dd3/src/main/kotlin/DataPack.kt#L16

I haven't done much with data packs personally, but the pack.mcmeta page makes it look like description is also JSON. Is that right? I might be missing something

BenWoodworth commented 1 year ago

Also that's an awesome project! I love what I'm seeing :grin:

Ayfri commented 1 year ago

Thanks ! It can be a string, a NBTList or a NBTCompound, as it is considered as a Text Component.

BenWoodworth commented 1 year ago

How's this look? Serializing seems to work like a charm. I played around with deserializing for a good two hours and... yeah. It's tricky... trying to figure out the best way to map things around :grimacing:

It seems like you only need to serialize for now though so here's a start:

import kotlinx.serialization.*
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.serialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import net.benwoodworth.knbt.*

private object NbtAsJsonTextComponentSerializer : KSerializer<NbtTag> {
    @OptIn(ExperimentalSerializationApi::class)
    override val descriptor: SerialDescriptor =
        SerialDescriptor("TextComponentJsonSerializer", serialDescriptor<JsonElement>())

    override fun serialize(encoder: Encoder, value: NbtTag): Unit =
        encoder.encodeSerializableValue(JsonElement.serializer(), value.toJsonElement())

    override fun deserialize(decoder: Decoder): NbtTag =
        throw UnsupportedOperationException("Deserializing not supported")

    private fun NbtTag.toJsonElement(): JsonElement = when (this) {
        is NbtCompound -> JsonObject(mapValues { it.value.toJsonElement() })
        is NbtList<*> -> JsonArray(map { it.toJsonElement() })
        is NbtByteArray -> JsonArray(map { JsonPrimitive(it) })
        is NbtIntArray -> JsonArray(map { JsonPrimitive(it) })
        is NbtLongArray -> JsonArray(map { JsonPrimitive(it) })

        is NbtString -> JsonPrimitive(value)

        is NbtByte -> when(value) { // Or just convert to number?
            0.toByte() -> JsonPrimitive(false)
            1.toByte() -> JsonPrimitive(true)
            else -> JsonPrimitive(value)
        }

        is NbtShort -> JsonPrimitive(value)
        is NbtInt -> JsonPrimitive(value)
        is NbtLong -> JsonPrimitive(value)
        is NbtFloat -> JsonPrimitive(value)
        is NbtDouble -> JsonPrimitive(value)
    }

//    private fun JsonElement.toNbtTag(): NbtCompound = when (this) {
//        is JsonObject -> NbtCompound(mapValues { (_, value) -> value })
//
//        is JsonArray -> // NbtList elements must all be the same type
//
//        is JsonPrimitive -> when (val value = contentOrNull) {
//            null -> error("$value is not supported in text components")
//            else -> NbtString(value)
//        }
//    }
}

@Serializable
data class Pack(
    var packFormat: Int,

    @Serializable(NbtAsJsonTextComponentSerializer::class)
    var description: NbtTag,
)
Ayfri commented 1 year ago

Nice, it works well, thank you so much ! Can't wait to have it integrated by default :)