Search code examples
kotlinkotlinx.serialization

Kotlin: Deserialize parts of JSON into nested child objects


Is it possible to deserialize a JSON structure so that portions of that structure are collected into a nested child object?

So given this JSON structure

{
  "root_field1": "This field will be in root",
  "root_field2": "This field will be in root",
  "child_field1": "This field will be in a child object",
  "child_field2": 123
}

Is it possible to use a JSONTransformSerializer (or some other way) to deserialize the above json into:

@Serializable
data class Root(
  @SerialName("root_field1")
  val field1: String,
  @SerialName("root_field2")
  val field2: String,
  val child: Child
)

@Serializable
data class Child(
  @SerialName("child_field1")
  val field1: String,
  @SerialName("child_field2")
  val field2: Int
)

I've attempted to use a JsonTransformingSerializer on the Root, however that simply causes an exception due to the child element not being found.

I also attempted to set the child to @Transient in hopes that would allow me to circumvent the issue, however the JsonTransformingSerializer still requires a KSerializer for its underlying class as an input and so that did not work.


Solution

  • Turns out that this was easily doable by combining both a JsonTransformingSerializer<Root> and a custom KSerializer<Root> (which I hadn't thought off before) in the following manner:

    object RootTransformingSerializer : JsonTransformingSerializer<Root>(RootSerializer) {
    
       private val childSet: Set<String> = setOf("child_field1", "child_field2")
    
       override fun transformDeserialize(element: JsonElement): JsonElement {
           val child: MutableMap<String, JsonElement> = mutableMapOf()
           return buildJsonObject() {
              if (element !is JsonObject) {
                    return element
              }
              element.forEach { entry ->
                 if (entry.key in childSet) {
                   child.put(entry.key, entry.value)
                 } else {
                   put(entry.key, entry.value)
                 }
              }
              put("child", JsonObject(child))
           }
       }
    }
    

    The custom KSerializer then looks like the following

    import kotlinx.serialization.descriptors.element
    
    object RootSerializer : KSerializer<Root> {
     
       override val descriptor: SerialDescriptor
            get() = buildClassSerialDescriptor("Root") {
               element<String>("root_field1")
               element<String>("root_field2")
               element<Child>("child")
            }
    
       override fun deserialize(decoder: Decoder): Commit {
            return decoder.decodeStructure(descriptor) {
               val rootField1 = decodeStringElement(descriptor, 0)
               val rootField2 = decodeStringElement(descriptor, 1)
               val child = decodeSerializableElement(descriptor, 2, Child.serializer())
               Root(rootField1, rootField2, child)
            }
    }