Kotlin: convert nested JSON object to literal string

2.1k views Asked by At

I have a data class which has a property whose type is another data class, like this:

@Serializable
data class Vehicle (
  val color: String,
  val miles: Int,
  val year: Int,
  val garage: Garage
)

@Serializable
data class Garage (
  val latitude: Float,
  val longitude: Float,
  val name: String
)

Upon serializing, it produces output like this:

{ 
  "color" : "black" , 
  "miles" : 35000 , 
  "year" : 2017 , 
  "garage" : { "latitude" : 43.478342 , "longitude" : -91.337000 , "name" : "Paul's Garage" }
}

However I would like garage to be a literal string of its JSON representation, not an actual JSON object. In other words, the desired output is:

{ 
  "color" : "black" , 
  "miles" : 35000 , 
  "year" : 2017 , 
  "garage" : "{ \"latitude\" : 43.478342 , \"longitude\" : -91.337000 , \"name\" : \"Paul's Garage\" }"
}

How can I accomplish this in Kotlin? Can it be done with just kotlinx.serialization or is Jackson/Gson absolutely necessary?

Note that this output is for a specific usage. I cannot overwrite the base serializer because I still need to serialize/deserialize from normal JSON (the first example). In other words, the best scenario would be to convert the first JSON sample to the second, not necessarily to have the data class produce the 2nd sample directly.

Thanks!

2

There are 2 answers

1
ElegyD On BEST ANSWER

Create a custom SerializationStrategy for Vehicle as follows:

val vehicleStrategy = object : SerializationStrategy<Vehicle> {
    override val descriptor: SerialDescriptor
        get() = buildClassSerialDescriptor("Vehicle") {
            element<String>("color")
            element<Int>("miles")
            element<Int>("year")
            element<String>("garage")
        }

    override fun serialize(encoder: Encoder, value: Vehicle) {
        encoder.encodeStructure(descriptor) {
            encodeStringElement(descriptor, 0, value.color)
            encodeIntElement(descriptor, 1, value.miles)
            encodeIntElement(descriptor, 2, value.year)
            encodeStringElement(descriptor, 3, Json.encodeToString(value.garage))
        }
    }
}

Then pass it to Json.encodeToString():

val string = Json.encodeToString(vehicleStrategy, vehicle)

Result:

{"color":"black","miles":35000,"year":2017,"garage":"{\"latitude\":43.47834,\"longitude\":-91.337,\"name\":\"Paul's Garage\"}"}

More info here

0
Aleksandr Baklanov On

Here is a solution with a custom serializer for Garage and an additional class for Vehicle.

Garage to String serializer:

object GarageToStringSerializer : KSerializer<Garage> {
    override val descriptor = PrimitiveSerialDescriptor("GarageToString", PrimitiveKind.STRING)
    override fun serialize(encoder: Encoder, value: Garage) = encoder.encodeString(Json.encodeToString(value))
    override fun deserialize(decoder: Decoder): Garage = Json.decodeFromString(decoder.decodeString())
}

Auxiliary class:

@Serializable
data class VehicleDto(
    val color: String,
    val miles: Int,
    val year: Int,
    @Serializable(GarageToStringSerializer::class)
    val garage: Garage
) {
    constructor(v: Vehicle) : this(v.color, v.miles, v.year, v.garage)
}

The demanded result can be received with:

Json.encodeToString(VehicleDto(vehicle))