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.