2

I'm trying to use the jackson-kotlin integration. Mostly it works nice but I'm having trouble with deserializing generic types. I tried to adapt the answer to this question: Jackson - Deserialize using generic class

    // create an object mapper
    val jsonFactory = JsonFactory()
    jsonFactory.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false)
    jsonFactory.configure(JsonParser.Feature.IGNORE_UNDEFINED, true)

    val objectMapper = ObjectMapper(jsonFactory)
    objectMapper.findAndRegisterModules()
    objectMapper.propertyNamingStrategy = PropertyNamingStrategy.SnakeCaseStrategy()
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)

    // simple generic type
    data class Inner(val meaningOfLife: Int)
    data class Outer<T>(val inner: T)

    val outer = Outer(Inner(42))

    val serialized = objectMapper.stringify(outer, true)
    println(serialized)

    // deserializing does not work using:
    // https://stackoverflow.com/questions/11664894/jackson-deserialize-using-generic-class
    val parsed = objectMapper.readValue<Outer<Inner>>(serialized, objectMapper.typeFactory.constructParametricType(Outer::class.java,Inner::class.java))

This throws an exception:

com.fasterxml.jackson.databind.JsonMappingException: Cannot deserialize Class io.inbot.common.ObjectMapperTest$should handle generics$Outer (of type local/anonymous) as a Bean
 at [Source: (String)"{
  "inner" : {
    "meaning_of_life" : 42
  }
}"; line: 1, column: 1]

    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:306)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:268)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
    at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
    at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:477)
    at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:4190)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4009)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3042)
    at io.inbot.common.ObjectMapperTest.should handle generics(ObjectMapperTest.kt:22)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:124)
    at org.testng.internal.Invoker.invokeMethod(Invoker.java:580)
    at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:716)
    at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:988)
    at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:125)
    at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:109)
    at org.testng.TestRunner.privateRun(TestRunner.java:648)
    at org.testng.TestRunner.run(TestRunner.java:505)
    at org.testng.SuiteRunner.runTest(SuiteRunner.java:455)
    at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:450)
    at org.testng.SuiteRunner.privateRun(SuiteRunner.java:415)
    at org.testng.SuiteRunner.run(SuiteRunner.java:364)
    at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52)
    at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:84)
    at org.testng.TestNG.runSuitesSequentially(TestNG.java:1208)
    at org.testng.TestNG.runSuitesLocally(TestNG.java:1137)
    at org.testng.TestNG.runSuites(TestNG.java:1049)
    at org.testng.TestNG.run(TestNG.java:1017)
    at org.testng.IDEARemoteTestNG.run(IDEARemoteTestNG.java:72)
    at org.testng.RemoteTestNGStarter.main(RemoteTestNGStarter.java:123)
Caused by: java.lang.IllegalArgumentException: Cannot deserialize Class io.inbot.common.ObjectMapperTest$should handle generics$Outer (of type local/anonymous) as a Bean
    at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.isPotentialBeanType(BeanDeserializerFactory.java:877)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:131)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:411)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:349)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:264)
    ... 31 more

Obviously the jackson plugin for kotlin does not handle this. Is there a workaround for this or a different way of doing this?

BTW. the stringify function is a simple extension function I added for ObjectMapper that takes care of the boilerplate code:

/**
 * Serializes [value] to a string. Pretty prints if [pretty] is set.
 */
fun <T> ObjectMapper.stringify(value: T, pretty: Boolean = false): String {
    val bos = ByteArrayOutputStream()
    val writer = OutputStreamWriter(bos, StandardCharsets.UTF_8)
    if(pretty) {
        writerWithDefaultPrettyPrinter().writeValue(writer,value)
    } else {
        writeValue(writer, value)
    }
    writer.flush()
    bos.flush()
    return bos.toByteArray().toString(StandardCharsets.UTF_8)
}

UPDATE The code in the answer by @jayson-minard works. It turns out that the key difference with my code was that I defined the data classes inside the test method. Moving them outside to the top level fixes things. Putting a dataclass in a function was a bad idea to begin with.

Jilles van Gurp
  • 7,927
  • 4
  • 38
  • 46
  • Have you tried this without your type factory call, and let the `readValue` extension function just infer the reified type? – Jayson Minard Nov 27 '18 at 12:32
  • What version of Jackson are you using? what version of the Jackson-Kotlin module are you using? what version of Kotlin? – Jayson Minard Nov 27 '18 at 13:13
  • 2.9.7 of both, type inference does not work here because jackson has no way of knowing what the type is. Also Java 8, kotlin 1.3.10 – Jilles van Gurp Nov 27 '18 at 15:09

1 Answers1

5

Using 2.9.6 of Jackson and then also the master branch 2.9.8 of Jackson along with the current Jackson-Kotlin module, I added this test case which passes for both your version of the code, and the cleaner idiomatic version of the code.

Note that I updated the stringify method to be more idiomatic as well, but this would not affect the test.

Your call to readValue is also more complicated than neccessary. Change:

val parsed = objectMapper.readValue<Outer<Inner>>(serialized, objectMapper.typeFactory.constructParametricType(Outer::class.java, Inner::class.java))

to simply:

val parsed = objectMapper.readValue<Outer<Inner>>(serialized)    

This is the full passing tests:

class TestStackOverflow53499407 {
    data class Inner(val meaningOfLife: Int)
    data class Outer<T>(val inner: T)

    fun <T> ObjectMapper.stringify(value: T, pretty: Boolean = false): String {
        StringWriter().use { writer ->
            if (pretty) {
                writerWithDefaultPrettyPrinter().writeValue(writer, value)
            } else {
                writeValue(writer, value)
            }
            return writer.toString()
        }
    }

    @Test
    fun test53499407_cleanTest() {
        val outer = Outer(Inner(42))
        val objectMapper = jacksonObjectMapper()

        val serialized = objectMapper.stringify(outer, true)
        println(serialized)

        val parsed = objectMapper.readValue<Outer<Inner>>(serialized)
        assertEquals(42, parsed.inner.meaningOfLife)
    }

    @Test
    fun test53499407_idiomatic_tweek() {
        val outer = Outer(Inner(42))

        val jsonFactory = JsonFactory()
        jsonFactory.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false)
        jsonFactory.configure(JsonParser.Feature.IGNORE_UNDEFINED, true)

        val objectMapper = ObjectMapper(jsonFactory)
        objectMapper.findAndRegisterModules()
        objectMapper.propertyNamingStrategy = PropertyNamingStrategy.SnakeCaseStrategy()
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)

        val serialized = objectMapper.stringify(outer, true)
        println(serialized)

        // This line changed to be idiomatic
        val parsed = objectMapper.readValue<Outer<Inner>>(serialized)
        assertEquals(42, parsed.inner.meaningOfLife)
    }

    @Test
    fun test53499407_as_written_in_stackoverflow() {
        val outer = Outer(Inner(42))

        val jsonFactory = JsonFactory()
        jsonFactory.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false)
        jsonFactory.configure(JsonParser.Feature.IGNORE_UNDEFINED, true)

        val objectMapper = ObjectMapper(jsonFactory)
        objectMapper.findAndRegisterModules()
        objectMapper.propertyNamingStrategy = PropertyNamingStrategy.SnakeCaseStrategy()
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)

        val serialized = objectMapper.stringify(outer, true)
        println(serialized)

        // deserializing does not work using:
        // https://stackoverflow.com/questions/11664894/jackson-deserialize-using-generic-class
        val parsed = objectMapper.readValue<Outer<Inner>>(serialized, objectMapper.typeFactory.constructParametricType(Outer::class.java, Inner::class.java))
        assertEquals(42, parsed.inner.meaningOfLife)
    }
}

enter image description here

Jayson Minard
  • 84,842
  • 38
  • 184
  • 227
  • Thanks for looking into this. I tried doing what you do with some variations of object mapper config and your nicer stringify but I always get this weird exception: java.lang.reflect.GenericSignatureFormatError: Signature Parse error: expected '<' or ';' but got Remaining input: handle generics$Outer;>; Deserializing to a non generic class works, i.e. nothing wrong with the string. So what's different on your setup? Java 8/kotlin 1.3.10 here. – Jilles van Gurp Nov 27 '18 at 15:08
  • I figured out the difference, putting the data class defiinitions in the test method as I did does not work. Moving them outside the function scope fixes things. Even my original version works that way. So it matters where you put the class definitions for deserialization. – Jilles van Gurp Nov 27 '18 at 15:15