https://kotlinlang.org logo
Title
a

Ayfri

02/02/2023, 10:37 AM
Hi, how could I serialize a collection of this structure :
data class Advancement(val name: String, val done: Boolean = false, val criteria: Map<String, Boolean> = mapOf())
As a map with advancements name as keys and
done
as value if
criteria
is empty, else as the map itself (totally ignoring
done
) ?
a

Adam S

02/02/2023, 11:10 AM
what about if
done
was a property of the class, and was annotated with
@EncodeDefault
?
@Serializable
data class Advancement(
  val name: String, 
  val criteria: Map<String, Boolean> = mapOf()
) {
  @OptIn(ExperimentalSerializationApi::class)
  @EncodeDefault
  val done: Boolean get() = criteria.isEmpty()
}
if
criteria
is empty, then it matches the default value, so shouldn’t be encoded. Whereas
done
will always be encoded, which doesn’t quite match your criteria, but I think it’s close enough. Anything more could be quite complicated!
a

Ayfri

02/02/2023, 1:02 PM
I can't because
done
can also be changed explicitly
a

Adam S

02/02/2023, 3:05 PM
Hmm… so what should happen if
done
is set to
false
and the map isn’t empty?
a

Ayfri

02/02/2023, 3:08 PM
Then the map take the priority
a

Adam S

02/02/2023, 3:13 PM
what about if
done
is set as
true
?
a

Ayfri

02/02/2023, 3:16 PM
If the map is not empty then it takes the priority. It's because I need a json entry where the value could be a boolean or an objet containing multiple boolean entries, so I've done it that way in Kotlin, maybe there's a better way but I don't know it then
a

Adam S

02/02/2023, 3:16 PM
can you share the Kotlin code? I’m finding it tough to figure out the logic
a

Ayfri

02/02/2023, 3:17 PM
There is no code for this right now, I didn't started to use it, I just created the data class you see upper
a

Adam S

02/02/2023, 3:17 PM
ah okay
a

Ayfri

02/02/2023, 3:19 PM
My JSON entry could be this :
"minecraft:my_advancement": true
Or this :
"minecraft:my_advancement": {
    "condition1": false,
    "condition2": true
}
a

Adam S

02/02/2023, 3:19 PM
since it’s JSON you could look into
JsonTransformingSerializer
https://github.com/Kotlin/kotlinx.serialization/blob/v1.4.1/docs/json.md#json-transformations
a

Ayfri

02/02/2023, 3:20 PM
yes I know, but I can't figure out how to create a serializer like this, that was was I posted my message, to get some help creating it
a

Adam S

02/02/2023, 3:20 PM
ahh okay, so the data is a key-value map, and the value can either be 1. a boolean or 2. a string-boolean map
a

Ayfri

02/02/2023, 3:21 PM
Exactly !
a

Adam S

02/02/2023, 3:27 PM
so basically this is ‘sealed polymorphism’. The data can have multiple types, but it’s restricted to a limited number of types. What’s usually nice in these situations is to be explicit and avoid generics (because generics are usually open polymorphism), and create a sealed interface that both types inherit
@Serializable
sealed interface Advancement {
  val name: String

  @Serializable
  data class Simple(
    override val name: String,
    val value: Boolean,
  ) : Advancement

  @Serializable
  data class Complex(
    override val name: String,
    val value: Map<String, Boolean>,
  ) : Advancement
}
but the problem with this is that Kotlinx Serialization can’t distinguish between them, so it will inject a discriminator field, and you’ll end up with
{ name: "some-achievement", value: true, type: "Simple" }
And if you’re decoding, then
type
won’t be present, and so KxS will fail To overcome that, you need a custom serializer, and that’s what
JsonContentPolymorphicSerializer
is for https://github.com/Kotlin/kotlinx.serialization/blob/v1.4.1/docs/json.md#content-based-polymorphic-deserialization
a

Ayfri

02/02/2023, 3:31 PM
Also I don't really need deserialization, if It's easy to implement then I'll implement it, but currently my program don't use deserialization at all !
b

Ben Woodworth

02/04/2023, 5:00 AM
How about this, delegating to a normal map serializer?
object AdvancementSerializer : SerializationStrategy<Advancement> {
    private val mapSerializer = MapSerializer(String.serializer(), Boolean.serializer())

    override val descriptor: SerialDescriptor =
        SerialDescriptor("my.package.Advancement", mapSerializer.descriptor)

    override fun serialize(encoder: Encoder, value: Advancement) {
        val map = if (value.criteria.isEmpty()) {
            mapOf(value.name to value.done)
        } else {
            value.criteria
        }

        encoder.encodeSerializableValue(mapSerializer, map)
    }
}
a

Ayfri

02/05/2023, 12:34 PM
Sorry but what is a SerializationStrategy ? How do I use it to specify the serializer of a property ?
b

Ben Woodworth

02/05/2023, 3:43 PM
Oh, KSerializer implements that and DeserializationStrategy, so it's just the serializing half. I'm not sure if there's a proper way, since
@Serializable
doesn't accept SerializationStrategy, but you can change my serializer to implement KSerializer and throw an error when deserializing
a

Ayfri

02/05/2023, 8:19 PM
I see, thanks !