skip to Main Content

I have the following generic sealed class representing the status of network response.

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "status")
@JsonSubTypes(
    value = [
        JsonSubTypes.Type(value = Response.OK::class, name = "OK"),
        JsonSubTypes.Type(value = Response.Error::class, name = "ERROR"),
    ]
)
sealed class Response<out Content, out Error> {
    data class OK<out Content>(val content: Content) : Response<Content, Nothing>()
    data class Error<out Error>(val error: Error) : Response<Nothing, Error>()
}

The network response json can have status": "OK" in which case it contains a content key or a "status": "ERROR" in which case it contains a error key.

The type under the content and error key can be different for each endpoint I’m talking to. Hence the need for generic types

So for example one endpoit returns String as types, so Response<String, String>

{
  "status": "OK",
  "content": "Hello"
}
{
  "status": "ERROR",
  "error": "MyError"
}

Another endpoit return Double as content and Int as error, so

Response<Double, Int>

{
  "status": "OK",
  "content": 2.0
}
{
  "status": "ERROR",
  "error": 1
}

My parsing fails though with message

Could not resolve type id 'OK' as a subtype of `com.example.models.Response<java.lang.String,java.lang.String>`: Failed to specialize base type com.example.models.Response<java.lang.String,java.lang.String> as com.example.models.Response$OK, problem: Type parameter #1/2 differs; can not specialize java.lang.String with java.lang.Object
 at [Source: (String)"{
  "status": "OK",
  "content": "Hello"
}"; line: 2, column: 13]
@Nested
inner class ContentParsingTests {
    @Test
    fun `parses OK response`() {
        val json = """
    {
      "status": "OK",
      "content": "Hello"
    }
    """.trimIndent()

        when (val result = objectMapper.readValue<Response<String, String>>(json)) {
            is Response.OK -> {
                assertEquals(result.content, "Hello")
            }
            is Response.Error -> {
                fail()
            }
        }
    }

    @Test
    fun `parses ERROR response`() {
        val json = """
        {
          "status": "ERROR",
          "error": "MyError"
        }
    """.trimIndent()

        when (val result = objectMapper.readValue<Response<String, String>>(json)) {
            is Response.OK -> {
                fail()
            }
            is Response.Error -> {
                assertEquals(result.error, "MyError")
            }
        }
    }
}

I noticed that the parsing works fine if only the content is generic:

sealed class Response<out Content > {
    data class OK<out Content>(val content: Content) : Response<Content>()
    object Error : Response<Nothing>()
}

but of course I loose the error payload

What would be a correct way to parse the json into my generic class?

2

Answers


  1. You don’t need generics at all for this case. Just have:

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "status")
    @JsonSubTypes(
        value = [
            JsonSubTypes.Type(value = Response.OK::class, name = "OK"),
            JsonSubTypes.Type(value = Response.Error::class, name = "ERROR"),
        ]
    )
    sealed interface Response {
      data class Success(val content: Content): Response
      data class Error(val error: Error): Response
    }
    

    Jackson will then be able to parse everything correctly.

    Login or Signup to reply.
  2. I think the issue is with Nothing because it’s like Void and you can’t create an instance of it or get a type information that’s why the serialization library struggling with it. so a solution for the current problem is to update the model definition like this and it works. It’s not ideal though.

    sealed class Response<out Content, out Error> {
    
        data class OK<out Content, out Error>(val content: Content) : Response<Content, Error>()
    
        data class Error<out Content, out Error>(val error: Error) : Response<Content, Error>()
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search