Reputation: 2025
Suppose I got an error from server as JSON which can be in two different structure:
{"errorMessage": "This action is unauthorized."}
OR
{"errorMessage":{"changePassword":"Old password is incorrect."}}
How can I deserialize this kind of json?
What I tried
I tried to have abstract class "Error" and two childs:
abstract class Error() {}
data class SingleError(val errorMessage: String) : Error()
data class MultiError(val errorMessage: Map<String, String>) : Error()
Then I try:
jacksonObjectMapper().readValue<Error>(response.body)
to deserialize, but I haave exception:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.fifth_llc.siply.main.request.Forbidden$Error` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (String)"{"errorMessage":{"changePassword":"Old password is incorrect."}}"; line: 1, column: 1]
Also I have tried JsonDeserialize annotaiton, but it seem I can use it if I want to parse to concrete type:
@JsonDeserialize(`as` = MultiError::class)
Any help?
Upvotes: 0
Views: 2597
Reputation: 2672
Custom deserializer approach:
sealed class Error() {
data class SingleError(val errorMessage: String) : Error()
data class MultiError(val errorMessage: Map<String, String>) : Error()
}
...
class ErrorDeserializer : StdDeserializer<Error>(Error::class.java) {
companion object {
private val MAP_TYPE_REFERENCE = object : TypeReference<Map<String, String>>() {}
}
@Throws(IOException::class, JsonProcessingException::class)
override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): Error {
val mapper = jp.codec as ObjectMapper
val node: JsonNode = mapper.readTree(jp)
val msgNode = node.get("errorMessage")
if (msgNode.isValueNode) {
val errorMsg = msgNode.asText()
return Error.SingleError(errorMsg)
} else {
val errorMsgs = mapper.readValue<Map<String, String>>(msgNode.toString(),
MAP_TYPE_REFERENCE)
return Error.MultiError(errorMsgs)
}
}
}
Usage:
val mapper = ObjectMapper()
val module = SimpleModule().addDeserializer(Error::class.java, ErrorDeserializer())
mapper.registerModule(module)
val error = mapper.readValue<Error>("json content", Error::class.java)
when (error) {
is Error.SingleError -> {
// error.errorMessage
}
is Error.MultiError -> {
// error.errorMessage
}
}
Upvotes: 2
Reputation: 10047
The problem is that Jackson either needs a JsonCreator or an empty constructor.
Data classes don't have empty constructors. If you want an empty constructor anyway, you can use the Kotlin No-Args plugin.
gradle:
dependencies {
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlinVersion"
...
}
...
apply plugin: "kotlin-noarg"
...
noArg {
annotation("com.your.package.NoArgs")
}
Now, create the Custom Annotation:
package com.your.package
annotation class NoArgs
Now the easiest * way is to use a generic type as your error message and use the custom annotation:
@NoArgs
data class Error<out T>(val errorMessage: T)
val x = ObjectMapper().readValue<Error<*>>(json1)
val y = ObjectMapper().readValue<Error<*>>(json2)
when(y.errorMessage) {
is String -> println("I'm a String!")
is Map<*,*> -> println("I'm a Map")
else -> println("I'm something else!")
}
You could also define the type explicitly on readValue
, if you know it in advance: readValue<Error<String>>
You could also create a method inside Error
for that logic in when
* There might be more elegant or useful solutions depending on your exact context.
Upvotes: 0
Reputation: 602
As far as I know, at least I experienced, you can not assign abstract type to readValue method, you have to specified it's concrete type both in readValue parameterizedType and it's properties.
jacksonObjectMapper().readValue<Error>(response.body)
In the line above you have to replace Error by one of it's subtype you wish and in following you must replace Map
bye HashedMap
data class MultiError(val errorMessage: Map<String, String>) : Error()
Another way is using @JsonTypeInfo into json object to notify JackSon to include exact object type as meta-data into json stream. this solution needs code correction in both server and client side application. Refer to following link:
Upvotes: 0