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
)
}
}