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.