Polymorphic JSON Parsing with Kotlin and Jackson

Jens
2 min readAug 27, 2024

--

Parsing JSON-encoded data can sometimes introduce unnecessary overhead, particularly when integrating data from third-party systems where JSON is transmitted typeless.

Photo by Ferenc Almasi on Unsplash

In this article, we’ll explore how to handle polymorphic JSON parsing using the Jackson library.

Polymorphic Parsing based on given Property

Let’s assume our application receives JSON data representing either an airplane or a car.

Plain payload:

        {
"type": "plain",
"uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"engines": 4,
"spread": 2.5
}

Car payload:

        {
"type": "car",
"uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"topSpeed": 250.0,
"manufacturer": "BMW"
}

Our goal is to parse this payload directly into the appropriate target class using a parser operation. Fortunately, Jackson provides us with a JsonTypeInfo annotation.

Let’s define our target class construct:

import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.annotation.JsonTypeName
import java.util.*

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes(
value = [
JsonSubTypes.Type(value = Plain::class, name = "plain"),
JsonSubTypes.Type(value = Car::class, name = "car")
]
)
sealed class Vehicles(open val uuid: UUID)

@JsonTypeName("plain")
data class Plain(
override val uuid: UUID,
val engines: Int,
val spread: Double
) : Vehicles(uuid)

@JsonTypeName("car")
data class Car(
override val uuid: UUID,
val topSpeed: Double,
val manufacturer: String
) : Vehicles(uuid)

Essentially, we’re instructing Jackson to use the “type” field as an identifier for the class name.

Within JsonSubTypes, we map each corresponding name to its respective target class. In our example, the type with the value “plain” is mapped to Plain::class, and “car” is mapped to Car::class.

Time to Test Polymorphic Parser

For a test, I have defined a quick Junit test case.

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.example.Car
import org.example.Plain
import org.example.Vehicles
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlin.test.assertTrue


class VehicleServiceTest {

private lateinit var objectMapper: ObjectMapper

val testPayloadPlain = """
{
"type": "plain",
"uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"engines": 4,
"spread": 2.5
}
""".trimIndent()

val testPayloadCar = """
{
"type": "car",
"uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"topSpeed": 250.0,
"manufacturer": "BMW"
}
""".trimIndent()

@BeforeEach
fun setUp() {
objectMapper = ObjectMapper().registerKotlinModule()
}


@Test
fun `#VehicleService - parse payload into plain`() {

val plain = objectMapper.readValue(testPayloadPlain, Vehicles::class.java)
assertTrue(plain is Plain)

}

@Test
fun `#VehicleService - parse payload into car`() {
val car = objectMapper.readValue(testPayloadCar, Vehicles::class.java)
assertTrue(car is Car)
}
}

Conclusion

With JsonTypeInfo and JsonSubTypes, Jackson offers a powerful tool that eliminates the need for manual type checks and complex conversions.

Additionally, it simplifies the creation of test cases while significantly improving the readability and maintainability of your codebase.

--

--

Jens

CTO & Co-Founder @fivesec | Entrepreneur | Software Engineer