Skip to content

Instantly share code, notes, and snippets.

@3v1n0
Last active September 25, 2023 18:15
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save 3v1n0/ecbc5e825e2921bd0022611d7046690b to your computer and use it in GitHub Desktop.
Save 3v1n0/ecbc5e825e2921bd0022611d7046690b to your computer and use it in GitHub Desktop.
Kotlin Map<String, Any?> (andy Any type in general) (de)serialization tests with both Binary (CBOR) and JSON support
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import kotlin.reflect.KType
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.full.starProjectedType
@Serializable
data class AnyValueSurrogate(
val type : String,
@Contextual
val value : Any?
)
@Serializable
object NoneType
object AnyValueSerializer : KSerializer<Any?> {
override val descriptor : SerialDescriptor = AnyValueSurrogate.serializer().descriptor
override fun serialize(encoder: Encoder, value: Any?) {
if (value != null) {
val valueClass = value::class
val valueType = valueClass.starProjectedType
val valueSerializer = serializer(valueType)
if (encoder is JsonEncoder && isTypePrimitive(valueType)) {
encoder.encodeJsonElement(Json.encodeToJsonElement(valueSerializer, value))
} else {
/* Would be nice to use valueSerializer.descriptor.serialName,
* but how to deserialize that to a type? */
val composite = encoder.beginCollection(descriptor, 2)
composite.encodeSerializableElement(descriptor, 0, serializer(), valueClass.java.name)
composite.encodeSerializableElement(descriptor, 1, valueSerializer, value)
composite.endStructure(descriptor)
}
} else {
if (encoder is JsonEncoder) {
encoder.encodeJsonElement(JsonNull)
} else {
val composite = encoder.beginCollection(descriptor, 2)
composite.encodeSerializableElement(descriptor, 1, serializer<NoneType?>(), null)
composite.endStructure(descriptor)
}
}
}
private fun isTypePrimitive(type : KType) : Boolean {
/* This can be replaced when using experimental API (via @ExperimentalSerializationApi) with:
* valueSerializer.descriptor.kind is PrimitiveKind */
if (type.isSubtypeOf(Number::class.starProjectedType))
return true
if (type.isSubtypeOf(String::class.starProjectedType))
return true
if (type.isSubtypeOf(Boolean::class.starProjectedType))
return true
return false
}
private fun getSerializerForTypeName(strType : String) : KSerializer<*> {
return try {
serializer(Class.forName(strType).kotlin.starProjectedType)
} catch (e: ClassNotFoundException) {
throw SerializationException(e.message)
}
}
override fun deserialize(decoder: Decoder): Any? {
if (decoder is JsonDecoder) {
val element = decoder.decodeJsonElement()
if (element is JsonNull)
return null
if (element is JsonPrimitive) {
if (element.isString)
return element.content
return try {
element.boolean
} catch (e: Throwable) {
try {
element.long
} catch (e: Throwable) {
element.double
}
}
// else if (element.content == "true" || element.content == "false")
// return element.boolean
// else if (element.content.contains('.'))
// return element.double
// else
// return element.long
} else if (element is JsonObject && "type" in element && "value" in element) {
element["type"].also { type ->
if (type is JsonPrimitive && type.isString) {
val valueSerializer = getSerializerForTypeName(type.content)
element["value"].also { value ->
if (value is JsonObject)
return Json.decodeFromJsonElement(valueSerializer, value)
}
}
}
}
throw SerializationException("Invalid Json element $element")
} else {
val composite = decoder.beginStructure(descriptor)
var index = composite.decodeElementIndex(descriptor)
if (index == CompositeDecoder.DECODE_DONE)
return null
val strType = composite.decodeStringElement(descriptor, index)
if (strType.isEmpty())
throw SerializationException("Unknown serialization type")
index = composite.decodeElementIndex(descriptor).also {
if (it != index + 1)
throw SerializationException("Unexpected element index!")
}
getSerializerForTypeName(strType).also { serializer ->
composite.decodeSerializableElement(descriptor, index, serializer).also {
composite.endStructure(descriptor)
return it
}
}
}
}
}
@Serializable
data class MySerializableData(
val mapValues : Map<String, @Serializable(with = AnyValueSerializer::class) Any?>)
@Serializable
@ExperimentalSerializationApi
data class OtherDataClass(
val intVal : Int,
val strVal : String,
@Serializable(with = AnyValueSerializer::class)
val anyVal : Any? = null,
)
/*
This is another option not to use MySerializable data as is, and so not making "mapValues" to be
the root of the serialized data
*/
@Serializable(with = AnySerializableValueSerializer::class)
data class AnySerializableValue(val value : Any?)
object AnySerializableValueSerializer : KSerializer<AnySerializableValue> {
override val descriptor: SerialDescriptor = AnyValueSurrogate.serializer().descriptor
override fun serialize(encoder: Encoder, value: AnySerializableValue) =
AnyValueSerializer.serialize(encoder, value.value)
override fun deserialize(decoder: Decoder): AnySerializableValue =
AnySerializableValue(AnyValueSerializer.deserialize(decoder))
}
typealias SerializableMapType = MutableMap<String, AnySerializableValue>
@Serializable
abstract class SerializableMap : SerializableMapType
object MapStringAnySerializer : KSerializer<MapStringAny> {
override val descriptor : SerialDescriptor = SerializableMap.serializer().descriptor
override fun serialize(encoder: Encoder, value: MapStringAny) {
val entries : SerializableMapType = mutableMapOf()
value.entries.forEach { entries[it.key] = AnySerializableValue(it.value) }
encoder.encodeSerializableValue(serializer(), entries)
}
override fun deserialize(decoder: Decoder): MapStringAny {
val map = mutableMapOf<String, Any?>()
decoder.decodeSerializableValue(serializer<SerializableMapType>()).forEach {
map[it.key] = it.value.value
}
return MapStringAny(map)
}
}
@Serializable(with = MapStringAnySerializer::class)
data class MapStringAny(val entries : Map<String, Any?> = emptyMap())
fun main() {
val map = mapOf<String, Any?>("Fooo" to "bar",
"One" to 1,
"Two dot zero" to 2.0,
"True" to true,
"None" to null,
"Data" to OtherDataClass(5, "Cinque"))
val mapData = MySerializableData(map)
val encoded = Json.encodeToString(mapData)
println(encoded)
val decoded = Json.decodeFromString<MySerializableData>(encoded)
println(decoded)
require(mapData == decoded)
val mapStringAny = MapStringAny(map)
val encoded2 = Json.encodeToString(mapStringAny)
println(encoded2)
val decoded2 = Json.decodeFromString<MapStringAny>(encoded2)
println(decoded2)
require(mapStringAny == decoded2)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment