0

I write some kind of Any type serializer, but I get issue with converting data classes to json. And I get this kind of error:

Caused by: kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 3: Encountered an unknown key 'value' at path: $[0]

Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys.

JSON input: [{"value":{"value":"Sheldon"}},{"value":{"value":"Penny"}},{"value":{"value":"Leonard"}},{"value":{"value":"Howard"}}]

I'm tryna set ignoreUnknownKeys like this, but I still get error:

val Student.json
    get() = Json {
        ignoreUnknownKeys = true
    }.encodeToString(this)

What I need to change, to avoid this issue?

Below I show my code:

@Serializable
class Student(
    val firstname: String,
    val surname: String
)

typealias StudentId = ItemId

val Student.json
    get() = Json {
        ignoreUnknownKeys = true
    }.encodeToString(this)
@Serializable
class Lesson(
    val name: String,
    val students: Array<GradeInfo> = emptyArray()
) {
    fun addStudent(studentId: StudentId) =
        Lesson(
            name,
            students + GradeInfo (studentId, null)
        )
}

@Serializable
class GradeInfo(
    val studentId: StudentId,
    val grade: Grade?
)

typealias LessonId = ItemId

val Lesson.json
    get() = Json.encodeToString(this)
typealias ItemId = String

@Serializable
class Item<E>(
    val elem: E,
    val id: ItemId
)

And serializer:

private fun Any?.toJsonPrimitive(): JsonPrimitive {
    return when (this) {
        null -> JsonNull
        is JsonPrimitive -> this
        is Boolean -> JsonPrimitive(this)
        is Number -> JsonPrimitive(this)
        is String -> JsonPrimitive(this)
        else -> throw Exception("${this::class}")
    }
}

private fun JsonPrimitive.toAnyValue(): Any? {
    val content = this.content
    return when {
        this.isString -> content
        content.equals("null", ignoreCase = true) -> null
        content.equals("true", ignoreCase = true) -> true
        content.equals("false", ignoreCase = true) -> false
        content.toIntOrNull() != null -> content.toInt()
        content.toLongOrNull() != null -> content.toLong()
        content.toDoubleOrNull() != null -> content.toDouble()
        else -> throw Exception("content:$content")
    }
}

private fun Any?.toJsonElement(): JsonElement {
    return when (this) {
        null -> JsonNull
        is JsonElement -> this
        is Boolean -> JsonPrimitive(this)
        is Number -> JsonPrimitive(this)
        is String -> JsonPrimitive(this)
        is Iterable<*> -> JsonArray(this.map { it.toJsonElement() })
        is Map<*, *> -> JsonObject(this.map { it.key.toString() to it.value.toJsonElement() }.toMap())
        is Item<*> -> JsonObject(mapOf(Pair("value", elem.toJsonElement())))
        is Student -> JsonObject(mapOf(Pair("value", firstname.toJsonPrimitive())))
        is Lesson -> JsonObject(mapOf(Pair("value", name.toJsonPrimitive())))
        else -> throw Exception("${this::class}=${this}}")
    }
}

private fun JsonElement.toAnyOrNull(): Any? {
    return when (this) {
        is JsonNull -> null
        is JsonPrimitive -> toAnyValue()
        is JsonObject -> this.map { it.key to it.value.toAnyOrNull() }.toMap()
        is JsonArray -> this.map { it.toAnyOrNull() }
    }
}

inline fun <reified T> serializer(): AnySerializer<T> = AnySerializer(T::class.java)

class AnySerializer<T>(private val klass: Class<T>) : KSerializer<T> {
    private val delegateSerializer = JsonElement.serializer()
    override val descriptor = delegateSerializer.descriptor

    override fun serialize(encoder: Encoder, value: T) {
        encoder.encodeSerializableValue(delegateSerializer, value.toJsonElement())
    }

    override fun deserialize(decoder: Decoder): T {
        val jsonPrimitive = decoder.decodeSerializableValue(delegateSerializer)
        return jsonPrimitive.toAnyOrNull()?.let { klass.cast(it) }
            ?: throw SerializationException("Failed to deserialize")
    }
}

I use it like this:

inline fun <reified T : Any> Route.repoRoutes(
    repo: Repo<T>
) {
    val serializer: AnySerializer<T> = serializer()
    val itemSerializer: AnySerializer<Item<T>> = serializer()
    val listItemSerializer = ListSerializer(itemSerializer)

    get {
        val elemList: List<Item<T>> = repo.read()
        if (elemList.isEmpty()) {
            call.respondText("No element found", status = HttpStatusCode.NotFound)
        } else {
            val elemJson = Json.encodeToString(listItemSerializer, elemList)
            call.respond(elemJson)
        }
    }
    get("{id}") {
        val id =
            call.parameters["id"] ?: return@get call.respondText(
                "Missing or malformed id",
                status = HttpStatusCode.BadRequest
            )
        val item =
            repo.read(id) ?: return@get call.respondText(
                "No element with id $id",
                status = HttpStatusCode.NotFound
            )
        val itemJson = Json.encodeToString(itemSerializer, item)
        call.respond(itemJson)
    }
    post("byId") {
        val ids = try {
            call.receive<List<String>>()
        } catch (e: Throwable) {
            return@post call.respondText(
                "Request body is not list id", status = HttpStatusCode.BadRequest
            )
        }
        val elements = Json.encodeToString(listItemSerializer, repo.read(ids))
        call.respond(elements)
    }
    post {
        val elemJson = call.receive<String>()
        val elem = Json.decodeFromString(serializer, elemJson)
        repo.create(elem)
        call.respondText(
            "Element stored correctly",
            status = HttpStatusCode.Created
        )
    }
    delete("{id}") {
        val id = call.parameters["id"]
            ?: return@delete call.respond(HttpStatusCode.BadRequest)
        if (repo.delete(id)) {
            call.respondText(
                "Element removed correctly",
                status = HttpStatusCode.Accepted
            )
        } else {
            call.respondText(
                "No element with id $id",
                status = HttpStatusCode.NotFound
            )
        }
    }
    put("{id}") {
        val id = call.parameters["id"] ?: return@put call.respondText(
            "Missing or malformed id",
            status = HttpStatusCode.BadRequest
        )
        repo.read(id) ?: return@put call.respondText(
            "No element with id $id",
            status = HttpStatusCode.NotFound
        )
        val newElementJson = call.receive<String>()
        val newElement = Json.decodeFromString(serializer, newElementJson)
        repo.update(id, newElement)
        call.respondText(
            "Element updates correctly",
            status = HttpStatusCode.Created
        )
    }
}
user2340612
  • 10,053
  • 4
  • 41
  • 66

0 Answers0