Skip to content

Instantly share code, notes, and snippets.

@alifhasnain
Last active February 22, 2023 18:23
Show Gist options
  • Save alifhasnain/c1896cb4483d5990be8de6a9242f086c to your computer and use it in GitHub Desktop.
Save alifhasnain/c1896cb4483d5990be8de6a9242f086c to your computer and use it in GitHub Desktop.
JSON parsing with various library and various method.

GSON

TypeAdapter

We can create custom type adapters for dirty JSON files. Suppose we have a JSON text like below:

{
  "id": 2,
  "name": "Alif Hasnain",
  "age": {
    "age": 24,
    "mature": true
  }
}

Or when it is not properly formatted the JSON can also be like below:

{
  "id": 2,
  "name": "Alif Hasnain",
  "age": []
}

So, to convert it into a Model class we have to write a custom parser or the app will crash. Model class for this json can be like below:

data class UserModel(  
  @SerializedName("id")  
  val id: Long,  
  @SerializedName("name")  
  val name: String,  
  @SerializedName("age")  
  val age: Age  
)  
  
data class Age (  
  @SerializedName("age")  
  val age: Int,  
  @SerializedName("mature")  
  val mature: Boolean  
)

As we can the that the Age parameter is giving different value different time. So the custom adapter for Age can be like below:

class AgeTypeAdapter: TypeAdapter<Age>() {

    override fun write(out: JsonWriter?, value: Age?) {
        out?.jsonValue(Gson().toJson(value))
    }

    override fun read(input: JsonReader?): Age? {
        var age = -1
        var isMature = false
        runCatching {
            input?.beginObject()
        }.getOrElse {
            input?.beginArray()
            input?.endArray()
            return null
        }
        while (input!!.hasNext()) {
            when (input.nextName()) {
                "age" -> age = input.nextInt()
                "mature" -> isMature = input.nextBoolean()
                else -> input.skipValue()
            }
        }
        input.endObject()
        return Age(age, isMature)
    }
}

Now, to use our own made adapter for parsing JSON we have to add the custom Adapter programmatically to the JSON Builder like below:

val gson = GsonBuilder().apply {
   registerTypeHierarchyAdapter(Age::class.java, AgeTypeAdapter())
}.create()

val result = gson.fromJson(jsonString2, UserModel::class.java)
Logger.json(gson.toJson(result))

Moshi

JsonReader

Parsing JSON with Moshi is similar as GSON. Guess we have the JSON formatted like below:

[
  {
    "id": 2,
    "name": "Alif Hasnain",
    "age": 24
  },
  {
    "id": 3,
    "name": "Atif Hasnain",
    "age": 6
  },
  {
    "id": 4,
    "name": "Akib Hasnain"
  }
]

As we can see that age field is optional. Or when there is no data it can return an empty json object. Model for User is like below:

data class User(
    val id: Long,
    val name: String,
    var age: Int = -1,
    val ageStage: AgeStage
)

enum class AgeStage {
    UNKNOWN, TODDLER, PRESCHOOL, GRADESCHOOLER, TEEN, YOUNG_ADULT, ADULT
}

Here the data for AgeStage is not directly providen in the JSON string. So, we can add logic to the manual JSON parser in this case.

//Get data from the parser method

val users = getUserList(JsonReader.of(Buffer().writeUtf8(jsonStringPerson)))
//OR
val users2 = getUserList(JsonReader.of(ByteArrayInputStream(jsonString.toByteArray()).source().buffer()))

private fun getUserList(reader: JsonReader): List<User> {

    val result = mutableListOf<UserModel>()

    runCatching {
        reader.beginArray()
    }.getOrElse {
        /*
        * To overcome if the provided json
        * is accidentally and object we skip all the values and end reading
        * */
        Logger.v("Error while parsing", it)
        reader.beginObject()
        while (reader.hasNext()) {
            reader.skipValue()
        }
        reader.endObject()
        return mutableListOf()
    }

    while (reader.hasNext()) {
        var id = -1L
        var name = ""
        var age = -1
        reader.beginObject()

        while (reader.hasNext()) {
            when (reader.nextName()) {
                "id" -> id = reader.nextLong()
                "name" -> name = reader.nextString()
                "age" -> age = reader.nextInt()
                else -> reader.skipValue()
            }
        }
        
        /*
        * Manually initialize the value for 
        * AgeStage from the data we get from age field
        * */
        val ageStage = when {
            age in 0..3 -> AgeStage.TODDLER
            age in 4..5 -> AgeStage.PRESCHOOL
            age in 6..12 -> AgeStage.GRADESCHOOLER
            age in 12..18 -> AgeStage.TEEN
            age in 18..21 -> AgeStage.YOUNG_ADULT
            age > 21 -> AgeStage.ADULT
            else -> AgeStage.UNKNOWN
        }
        reader.endObject()
        result.add(User(id, name, age, ageStage))
    }
    reader.endArray()
    return result
}

Codegen

Example #1:

{
    "vote_count": 2026,
    "id": 19404,
    "title": "Example Movie",
    "image_path": "/this-is-an-example-movie-image.jpg",
    "genre_ids": [
      35,
      18,
      10749
    ],
    "overview": "Overview of example movie"
}

Here we can see that Genre only provides ids for the genre of the movie. To work convinently we can generate the name of the genre in String for the specific ids. Adapter for parsing:

class GenreAdapter {

    @ToJson
    fun toJson(genres: List<Genre>): List<Int> {
        return genres.map { genre -> genre.id}
    }

    @FromJson
    fun fromJson(genreId: Int): Genre = when (genreId) {
        28 -> Genre(28, "Action")
        12 -> Genre(12, "Adventure")
        16 -> Genre(16, "Animation")
        35 -> Genre(35, "Comedy")
        80 -> Genre(80, "Crime")
        18 -> Genre(18, "Drama")
        10749 -> Genre(10749, "Romance")
        53 -> Genre(53, "Mystery")
        10752 -> Genre(10752, "War")
        37 -> Genre(37, "Western")
        else -> throw JsonDataException("unknown genre id: $genreId")
    }
}

Code:

val moshi = Moshi.Builder().apply {
    add(GenreAdapter())
}.build()

val adapter = moshi.adapter(Movie::class.java)
val movie = adapter.fromJson(movieJson)
Logger.d(movie)

Example #2

Example of custom adapter form JSON keys in key value pair.

@JsonClass(generateAdapter = true)
data class Movie (
    ...
    ......
    val overview: OverView
)
@JsonClass(generateAdapter = true)
data class OverView (
    val test1: String,
    val test2: String
)

If the object is in key value pair we get it as map in the adapter. So, the adapter class we be like below:

class OverViewAdapter() {

    @ToJson
    fun toJson(overview: OverView): Map<String, String> {
        val map = mutableMapOf<String, String>()
        map["test1"] = overview.test1
        map["test2"] = overview.test2
        return map
    }

    @FromJson
    fun fromJson(overview: Map<String, String>): OverView {
        return OverView(overview["test1"]!!, overview["test2"]!!)
    }
}

JsonTypeAdapter

JsonTypeAdapter can be used to create adapter with custom logic. The example below will demonstrate how can we convert a JSON to Kotlin's Sealed class.

[
  {
    "userId": 1,
    "userType": "student",
    "student_id": "171-15-8966"
  },
  {
    "userId": 2,
    "userType": "teacher",
    "teacher_id": "312388192"
  },
  {
    "userId": 3,
    "userType": "student",
    "student_id": "171-15-1234"
  },
  {
    "userId": 4,
    "userType": "student",
    "student_id": "171-15-1111"
  }
]

Model:

sealed class User {
    data class Student (val userId: Int, val studentId: String): User()
    data class Teacher (val userId: Int, val teacherId: String): User()
}

Adapter:

class UserAdapter: JsonAdapter<User>() {

    override fun fromJson(reader: JsonReader): User? {
        reader.beginObject()
        var userId = 0
        var studentId: String? = null
        var teacherId: String? = null
        var userType: String? = null
        while (reader.hasNext()) {
            when (reader.nextName()) {
                "userId" -> userId = reader.nextInt()
                "student_id" -> studentId = reader.nextString()
                "teacher_id" -> teacherId = reader.nextString()
                "userType" -> userType = reader.nextString()
                else -> reader.skipValue()
            }
        }
        reader.endObject()
        return if (userType == "student") {
            User.Student(userId, studentId!!)
        } else {
            User.Teacher(userId, teacherId!!)
        }
    }

    override fun toJson(writer: JsonWriter, value: User?) {
        writer.beginObject()
        when (value) {
            is User.Student -> {
                writer.name("userId")
                writer.value(value.userId)
                writer.name("userType")
                writer.value("student")
                writer.name("student_id")
                writer.value(value.studentId)
            }
            is User.Teacher -> {
                writer.name("userId")
                writer.value(value.userId)
                writer.name("userType")
                writer.value("teacher")
                writer.name("teacher_id")
                writer.value(value.teacherId)
            }
        }
        writer.endObject()
    }
}

Code:

val moshi = Moshi.Builder().apply {
    addAdapter(UserAdapter())
}.build()

val type = Types.newParameterizedType(List::class.java, User::class.java)
val adapter = moshi.adapter<List<User>>(type)
val userList = adapter.fromJson(jsonString)
Logger.d(userList)
Logger.json(adapter.toJson(userList))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment