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?
CodePudding user response:
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>()
}
CodePudding user response:
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.