Devabc
Devabc

Reputation: 5271

Jackson Kotlin - Deserialize JsonNode

Problem

I have JSON content in the form of a string, which I first want to traverse programmatically with Jackson. Then, when I have the node of interest, I want to deserialize it.

What I have tried

I have successfully deserialized strings using mapper.readValue, but now I want to perform such an operation on a jsonNode instead of a string.

Libraries

Code

package somepackage

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.treeToValue

fun main() {
    val mapper = ObjectMapper().registerModule(KotlinModule())

    readValueWorksFine(mapper)
    treeToValueFails(mapper)
}

fun treeToValueFails(mapper: ObjectMapper) {
    val fullJsonContent = """
            [{
                    "product_id":123, 
                    "Comments":
                        [{
                            "comment_id": 23, 
                            "message": "Hello World!"
                        }]
            }]        
        """.trimIndent()

    // Traverse to get the node of interest
    val commentsNode: JsonNode = mapper.readTree(fullJsonContent).get(0).get("Comments")

    // Deserialize
    val comments: List<Comment> = mapper.treeToValue<List<Comment>>(commentsNode)

    // The line below fails. (I would have expected the exception to be thrown in the line above instead.
    // Exception:
    // Exception in thread "main" java.lang.ClassCastException: class
    // java.util.LinkedHashMap cannot be cast to class somepackage.Comment (java.util.LinkedHashMap is in module
    // java.base of loader 'bootstrap'; somepackage.Comment is in unnamed module of loader 'app')
    for (comment: Comment in comments) { // This line fails
        println(comment.comment_id)
        println(comment.message)
    }
}

fun readValueWorksFine(mapper: ObjectMapper) {
    val commentsJsonContent = """
            [{
                "comment_id": 23, 
                "message": "Hello World!"
            }]
        """.trimIndent()

    val comments1: List<Comment> = mapper.readValue<List<Comment>>(commentsJsonContent)
    for (comment in comments1) {
        println(comment)
    }
}

data class Comment(val comment_id: Long, val message: String)

Exception/Output

The code above results in the following exception/output:

Comment(comment_id=23, message=Hello World!)
Exception in thread "main" java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class somepackage.Comment (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; somepackage.Comment is in unnamed module of loader 'app')
    at somepackage.TKt.treeToValueFails(T.kt:39)
    at somepackage.TKt.main(T.kt:13)
    at somepackage.TKt.main(T.kt)

Upvotes: 4

Views: 13439

Answers (1)

Devabc
Devabc

Reputation: 5271

The problem cause

Even though ObjectMapper.treeToValue is a Kotlin inline extension function with a reified generic parameter (which means that generics are preserved at runtime), it calls the Java ObjectMapper.treeToValue(TreeNode, Class<T>) method. The value passed as Class<T> will loose generic type information for generic types such as List<Comment>, because of type erasure.

So treeToValue can be used for:

mapper.treeToValue<Comment>(commentNode)

but not for:

mapper.treeToValue<List<Comment>>(commentsNode)

Also note that ObjectMapper contains multiple methods that have @SuppressWarnings annotations, which causes some problems not to appear at compile-time, but at run-time.

Solution 1 - use convertValue()

This is the best solution. It uses the Kotlin extension function ObjectMapper.convertValue.

val commentsNode = mapper.readTree(fullJsonContent).get(0).get("Comments")
val comments = mapper.convertValue<List<Comment>>(commentsNode)

Solution 2 - use an ObjectReader

This solution doesn't use jackson-module-kotlin extension functions.

val reader = mapper.readerFor(object : TypeReference<List<Comment>>() {})
val comments: List<Comment> = reader.readValue(commentsNode)

Solution 3 - deserialize in map

Because treeToValue (Kotlin extension function) does work for non-generic types, you can first get the nodes as as list of JsonNodes, and then map each JsonNode to a Comment.

But it's cumbersome that you cannot simply return mapper.treeToValue(it), because that causes type inference compile errors.

val commentsNode = mapper.readTree(fullJsonContent).get(0).get("Comments")
val comments = commentsNode.elements().asSequence().toList().map {
    val comment: Comment = mapper.treeToValue(it)
    comment
}

Upvotes: 5

Related Questions