Is there a way to deserialize Json into Kotlin data class and convert property types?

4 min read 05-10-2024
Is there a way to deserialize Json into Kotlin data class and convert property types?


Deserializing JSON with Type Conversion in Kotlin: A Comprehensive Guide

Problem: You have a JSON string containing data that doesn't perfectly match the data types of your Kotlin data class. You need to deserialize this JSON into your data class while converting the incoming data types to the desired ones.

Scenario: Imagine you have a JSON response like this:

{
  "id": 123,
  "name": "John Doe",
  "age": "30"
}

And you want to deserialize it into a Kotlin data class:

data class User(
  val id: Int,
  val name: String,
  val age: Int
)

Notice the age property in the JSON is a string, while in the User data class, it's an Int.

Solution: While Kotlin's built-in libraries like Gson or Moshi can deserialize JSON into data classes, they might not handle type conversion directly. You'll need to implement custom deserialization logic.

Implementation: Here are three common approaches:

1. Custom Deserializer with Gson:

import com.google.gson.Gson
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer

class UserDeserializer : JsonDeserializer<User> {
  override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): User {
    val id = json.asJsonObject.get("id").asInt
    val name = json.asJsonObject.get("name").asString
    val age = json.asJsonObject.get("age").asString.toInt()
    return User(id, name, age)
  }
}

class UserSerializer : JsonSerializer<User> {
  override fun serialize(src: User?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
    return JsonPrimitive(src.toString())
  }
}

fun main() {
  val gson = GsonBuilder()
      .registerTypeAdapter(User::class.java, UserDeserializer())
      .registerTypeAdapter(User::class.java, UserSerializer())
      .create()

  val jsonString = """
    {
      "id": 123,
      "name": "John Doe",
      "age": "30"
    }
  """

  val user: User = gson.fromJson(jsonString, User::class.java)
  println(user) // Output: User(id=123, name=John Doe, age=30)
}

In this example, we create a custom UserDeserializer that overrides the deserialize method. Inside this method, we manually extract and convert the JSON properties to the desired data types before constructing a User object. We also create a custom UserSerializer to handle serialization.

2. Custom Deserializer with Moshi:

import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory

data class User(
  val id: Int,
  val name: String,
  val age: Int
)

class UserAdapter {
  @FromJson
  fun fromJson(json: JsonReader): User {
    val id = json.nextInt()
    json.nextName()
    val name = json.nextString()
    json.nextName()
    val age = json.nextString().toInt()
    return User(id, name, age)
  }

  @ToJson
  fun toJson(user: User): JsonWriter {
    return JsonWriter.of(StringBuilder()).apply {
      beginObject()
      name("id")
      value(user.id)
      name("name")
      value(user.name)
      name("age")
      value(user.age)
      endObject()
    }
  }
}

fun main() {
  val moshi = Moshi.Builder()
      .add(KotlinJsonAdapterFactory())
      .add(UserAdapter())
      .build()
  val adapter = moshi.adapter(User::class.java)

  val jsonString = """
    {
      "id": 123,
      "name": "John Doe",
      "age": "30"
    }
  """
  val user = adapter.fromJson(jsonString)
  println(user) // Output: User(id=123, name=John Doe, age=30)
}

Here, we create a UserAdapter class with @FromJson and @ToJson annotations to handle deserialization and serialization, respectively. We manually parse the JSON and perform the necessary type conversions within the fromJson method.

3. Using Kotlin Reflection:

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlin.reflect.KClass
import kotlin.reflect.full.createInstance
import kotlin.reflect.full.memberProperties

data class User(
  val id: Int,
  val name: String,
  val age: Int
)

fun <T : Any> deserializeWithConversion(json: String, clazz: KClass<T>): T {
  val jsonObject = Json.parseToJson(json)
  val instance = clazz.createInstance()
  for (property in clazz.memberProperties) {
    val jsonValue = jsonObject[property.name]
    if (jsonValue != null) {
      val convertedValue = when (property.returnType.classifier) {
        Int::class -> jsonValue.primitive.int
        String::class -> jsonValue.primitive.content
        else -> jsonValue.primitive.content
      }
      property.setter.call(instance, convertedValue)
    }
  }
  return instance
}

fun main() {
  val jsonString = """
    {
      "id": 123,
      "name": "John Doe",
      "age": "30"
    }
  """

  val user = deserializeWithConversion(jsonString, User::class)
  println(user) // Output: User(id=123, name=John Doe, age=30)
}

In this example, we use Kotlin reflection to access the properties of the User data class. We iterate through the properties, retrieve the corresponding values from the JSON object, perform type conversions based on the property's type, and then set the values to the User instance.

Choosing the Best Approach:

  • Custom Deserializer with Gson or Moshi: Offers more flexibility for complex type conversions and custom logic.
  • Kotlin Reflection: Provides a generic way to handle type conversions for any data class, but might be less performant.

Key Takeaways:

  • Type Conversion: The primary challenge is to convert the data types coming from JSON to the desired types in your data class.
  • Flexibility: Choose the approach that offers the flexibility and performance required for your specific situation.
  • Error Handling: Implementing robust error handling for invalid data or type conversion failures is crucial.

References:

This article provides a comprehensive guide to deserializing JSON into Kotlin data classes with type conversions. By understanding these techniques and choosing the right approach, you can effectively handle data with differing types from external sources.