How to deserialize JSON object into Kotlin Pair with Jackson?

Viewed 4515

I have a simple data class representing a city

data class City(
    val name: String,
    val centroid: Coordinates
)

For external compatibility reasons the Coordinates type is defined as a typealias

typealias Coordinates = Pair<Double, Double>

val Coordinates.lat
    get() = first

val Coordinates.lon
    get() = second

How can I configure Jackson so that it's able to deserialize following JSON into an instance of City?

{
  "name": "Praha",
  "centroid": {
    "lat": 50.2141,
    "lon": 14.42342
  }
}
1 Answers

If you really have to preserve compatibility you could write your own Serializer & Deserializer, for example:

object CoordinatesConversions {

    const val LATITUDE_FIELD_NAME = "lat"
    const val LONGITUDE_FIELD_NAME = "lon"

    object Serializer : JsonSerializer<Coordinates>() {
        override fun serialize(value: Coordinates, gen: JsonGenerator, serializers: SerializerProvider) {
            with(gen) {
                writeStartObject()
                writeNumberField(LATITUDE_FIELD_NAME, value.first)
                writeNumberField(LONGITUDE_FIELD_NAME, value.second)
                writeEndObject()
            }
        }
    }

    object Deserializer : JsonDeserializer<Coordinates>() {
        override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Coordinates {
            val node = p.readValueAsTree<JsonNode>()

            val lat = node.get(LATITUDE_FIELD_NAME).asDouble()
            val lon = node.get(LONGITUDE_FIELD_NAME).asDouble()

            return Coordinates(lat, lon)
        }
    }
}

Also specify which serializers should be used for your City model

data class City(
    val name: String,

    @JsonSerialize(using = CoordinatesConversions.Serializer::class)
    @JsonDeserialize(using = CoordinatesConversions.Deserializer::class)
    val center: Coordinates
)

A fully working example:

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue

private typealias Coordinates = Pair<Double, Double>

object CoordinatesConversions {

    const val LATITUDE_FIELD_NAME = "lat"
    const val LONGITUDE_FIELD_NAME = "lon"

    object Serializer : JsonSerializer<Coordinates>() {
        override fun serialize(value: Coordinates, gen: JsonGenerator, serializers: SerializerProvider) {
            with(gen) {
                writeStartObject()
                writeNumberField(LATITUDE_FIELD_NAME, value.first)
                writeNumberField(LONGITUDE_FIELD_NAME, value.second)
                writeEndObject()
            }
        }
    }

    object Deserializer : JsonDeserializer<Coordinates>() {
        override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Coordinates {
            val node = p.readValueAsTree<JsonNode>()

            val lat = node.get(LATITUDE_FIELD_NAME).asDouble()
            val lon = node.get(LONGITUDE_FIELD_NAME).asDouble()

            return Coordinates(lat, lon)
        }
    }
}

object StackOverflow {

    private val objectMapper = jacksonObjectMapper().apply {
        configure(SerializationFeature.INDENT_OUTPUT, true)
    }

    data class City(
        val name: String,

        @JsonSerialize(using = CoordinatesConversions.Serializer::class)
        @JsonDeserialize(using = CoordinatesConversions.Deserializer::class)
        val center: Coordinates
    )

    @JvmStatic
    fun main(args: Array<String>) {

        val city = City("Cair Paravel", 13.37 to 42.0)

        val jsonOutput = objectMapper.writeValueAsString(city)
        println(jsonOutput)

        val deserializedCity = objectMapper.readValue<City>(jsonOutput)
        println(deserializedCity)
    }
}

I still think it would be better to try and move away from using Pairs to represent such a concrete data structure and I think it would benefit you in the future.

For example, it's not always clear which standard that's being used when representing a geographical point, some vendors use (lat, lng), others use (lng, lat). Even though you have defined named property getters on top of the alias, you wouldn't need to go through the hassle of defining a custom serializer and deserializer which is always nice.

Related