FasterXML / jackson-module-kotlin

Module that adds support for serialization/deserialization of Kotlin (http://kotlinlang.org) classes and data classes.
Apache License 2.0
1.11k stars 175 forks source link

Polymorphic sealed class missing type information #765

Open MaxDaten opened 5 months ago

MaxDaten commented 5 months ago

Your question

Hi,

maybe I'm missing some important piece or my thinking is a bit too naïve, but I try to write a generic message passing library based on a json protocol.

package com.fasterxml.jackson.module.kotlin.test.github

import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.exc.MismatchedInputException
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import junit.framework.TestCase.assertEquals
import org.junit.Assert.assertThrows
import kotlin.test.Test

class ReproductionTest {

    @JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME, include = JsonTypeInfo.As.PROPERTY, property = "@type")
    sealed class Response<out Err, out B>{
        data class Success<out B>(val response: B) : Response<Nothing, B>()
        data class Failure<out Err>(val error: Err) : Response<Err, Nothing>()
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME, include = JsonTypeInfo.As.PROPERTY, property = "@type")
    sealed class Message {
        data class Text(val text: String) : Message()
        data class Image(val url: String) : Message()
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME, include = JsonTypeInfo.As.PROPERTY, property = "@type")
    sealed class Error {
        data class NotFound(val message: String) : Error()
        data class BadRequest(val message: String) : Error()
    }

    val asJson = """{"@type":"Success","response":{"@type":"Text","text":"foo"}}"""
    val responseSuccess: Response<Error, Message> = Response.Success(Message.Text("foo"))

    @Test
    fun testReadAsValue() {
        val mapper = jacksonObjectMapper()
        val refType = mapper.typeFactory.constructParametricType(Response::class.java, Error::class.java, Message::class.java)

        // Failing com.fasterxml.jackson.databind.exc.InvalidTypeIdException:
        // Could not resolve type id 'Success' as a subtype of ...
        assertEquals(
            responseSuccess,
            mapper.readValue<Response<Error, Message>>(asJson, refType)
        )
    }

    @Test
    fun testWriteAsValue() {
        val mapper = jacksonObjectMapper()

        // Failing
        assertEquals(
            asJson,
            mapper.writeValueAsString(responseSuccess) // actual: {"@type":"Success","response":{"text":"foo"}}
        )
    }
}

My expectations/goals would be:

  1. if defined at the root type of Err/B type information should be included in the "response": {...} value and should be in control of the consuming code side (Message & Error would be defined there). But the type information is missing the the serialized value
  2. and of course it should be deserializable

Is that possible to achieve? I tried various variations, including mapper.writerFor(refType), without any success.

Help is appreciated!

Greetings