PCSalt
YouTube GitHub
Back to Kotlin
Kotlin · 3 min read

JSON Parsing in Kotlin — Parsing with Gson

Parse a complex JSON response using Gson with Kotlin data classes. Automatic mapping, @SerializedName, nullable types, and a custom TypeAdapter for Instant.


This is Part 2 of the JSON Parsing in Kotlin series. In Part 1, we parsed everything by hand with org.json — about 80 lines of explicit getString() calls. Now let’s see what happens when we let Gson do the heavy lifting.

Looking for the Java version? Read it here.

The JSON

Same JSON as the previous post — a SaaS API response with users, subscriptions, teams, and pagination.

{
  "status": "success",
  "data": {
    "users": [
      {
        "id": "usr_a1b2c3d4",
        "name": "Navkrishna",
        "email": "[email protected]",
        "role": "ADMIN",
        "verified": true,
        "createdAt": "2026-01-15T10:30:00Z",
        "profile": {
          "avatar": "https://cdn.example.com/avatars/nav.jpg",
          "bio": "Full-stack developer",
          "social": {
            "github": "krrishnaaaa",
            "twitter": null,
            "linkedin": "navkrishna"
          }
        },
        "subscription": {
          "plan": "PRO",
          "billingCycle": "YEARLY",
          "price": 199.99,
          "features": ["analytics", "api_access", "priority_support"],
          "trialEndsAt": null
        },
        "teams": [
          {
            "id": "team_x1y2",
            "name": "Backend",
            "role": "OWNER",
            "memberCount": 5
          },
          {
            "id": "team_z3w4",
            "name": "Mobile",
            "role": "MEMBER",
            "memberCount": 3
          }
        ],
        "lastLoginAt": "2026-03-14T18:45:00Z"
      },
      {
        "id": "usr_e5f6g7h8",
        "name": "Priya Sharma",
        "email": "[email protected]",
        "role": "MEMBER",
        "verified": false,
        "createdAt": "2026-03-01T14:00:00Z",
        "profile": {
          "avatar": null,
          "bio": null,
          "social": {
            "github": "priya-dev",
            "twitter": "priyacodes",
            "linkedin": null
          }
        },
        "subscription": {
          "plan": "TRIAL",
          "billingCycle": null,
          "price": 0.0,
          "features": ["analytics"],
          "trialEndsAt": "2026-03-31T23:59:59Z"
        },
        "teams": [],
        "lastLoginAt": null
      }
    ],
    "pagination": {
      "page": 1,
      "perPage": 20,
      "totalItems": 2,
      "totalPages": 1,
      "hasNext": false
    }
  },
  "meta": {
    "requestId": "req_9k8j7h6g",
    "timestamp": "2026-03-15T00:00:00Z",
    "apiVersion": "v2"
  }
}

Setup

Add Gson to your project. Maven:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.11.0</version>
</dependency>

Gradle:

implementation 'com.google.code.gson:gson:2.11.0'

Android note: Gson is already widely used in Android projects. Just add the Gradle dependency above — it works out of the box.

Model Classes

With Gson, the data classes are almost the same as before — but now we add @SerializedName annotations where the JSON key doesn’t match Kotlin naming conventions.

import com.google.gson.annotations.SerializedName
import java.time.Instant

data class Social(
  val github: String?,
  val twitter: String?,
  val linkedin: String?
)

data class Profile(
  val avatar: String?,
  val bio: String?,
  val social: Social
)

data class Subscription(
  val plan: String,
  @SerializedName("billingCycle")
  val billingCycle: String?,
  val price: Double,
  val features: List<String>,
  @SerializedName("trialEndsAt")
  val trialEndsAt: Instant?
)

data class Team(
  val id: String,
  val name: String,
  val role: String,
  @SerializedName("memberCount")
  val memberCount: Int
)

data class User(
  val id: String,
  val name: String,
  val email: String,
  val role: String,
  val verified: Boolean,
  @SerializedName("createdAt")
  val createdAt: Instant,
  val profile: Profile,
  val subscription: Subscription,
  val teams: List<Team>,
  @SerializedName("lastLoginAt")
  val lastLoginAt: Instant?
)

data class Pagination(
  val page: Int,
  @SerializedName("perPage")
  val perPage: Int,
  @SerializedName("totalItems")
  val totalItems: Int,
  @SerializedName("totalPages")
  val totalPages: Int,
  @SerializedName("hasNext")
  val hasNext: Boolean
)

data class Meta(
  @SerializedName("requestId")
  val requestId: String,
  val timestamp: Instant,
  @SerializedName("apiVersion")
  val apiVersion: String
)

data class Data(
  val users: List<User>,
  val pagination: Pagination
)

data class ApiResponse(
  val status: String,
  val data: Data,
  val meta: Meta
)

A couple of things to notice:

  • @SerializedName is optional here since the JSON keys already match the Kotlin property names (Gson matches by exact name). We add them explicitly on camelCase fields to be safe — if someone renames the property, the JSON mapping won’t break.
  • Nullable types (String?, Instant?) — Gson will set these to null when the JSON value is null. But be careful: Gson can also set non-nullable Kotlin properties to null at runtime since it bypasses the constructor. More on that below.
  • We added a Data class — unlike the org.json version where we manually unwrapped data.users, Gson maps the full structure, so we need a class for the data wrapper.

Custom TypeAdapter for Instant

Gson doesn’t know how to deserialize ISO 8601 strings into java.time.Instant out of the box. We need a custom TypeAdapter.

import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import java.time.Instant

class InstantTypeAdapter : TypeAdapter<Instant?>() {

  override fun write(out: JsonWriter, value: Instant?) {
    if (value == null) {
      out.nullValue()
    } else {
      out.value(value.toString())
    }
  }

  override fun read(reader: JsonReader): Instant? {
    if (reader.peek() == JsonToken.NULL) {
      reader.nextNull()
      return null
    }
    return Instant.parse(reader.nextString())
  }
}

Parsing

Here’s the entire parsing code:

import com.google.gson.GsonBuilder
import java.time.Instant

object GsonParser {

  private val gson = GsonBuilder()
    .registerTypeAdapter(Instant::class.java, InstantTypeAdapter())
    .create()

  fun parse(jsonString: String): ApiResponse {
    return gson.fromJson(jsonString, ApiResponse::class.java)
  }
}

That’s it. 3 lines of setup and a one-line parse call. Compare that to the ~80 lines of org.json parsing from Part 1.

Putting It Together

import java.nio.file.Path
import kotlin.io.path.readText

fun main() {
  val json = Path.of("response.json").readText()
  val response = GsonParser.parse(json)

  println("Status: ${response.status}")
  println("API Version: ${response.meta.apiVersion}")
  println("Total users: ${response.data.pagination.totalItems}")
  println()

  for (user in response.data.users) {
    println("--- ${user.name} ---")
    println("  Email: ${user.email}")
    println("  Role: ${user.role}")
    println("  Verified: ${user.verified}")
    println("  Plan: ${user.subscription.plan}")
    println("  Price: $${user.subscription.price}")
    println("  Features: ${user.subscription.features}")
    println("  GitHub: ${user.profile.social.github}")
    println("  Teams: ${user.teams.size}")
    for (team in user.teams) {
      println("    - ${team.name} (${team.role})")
    }
    println("  Last login: ${user.lastLoginAt}")
    println()
  }
}

Output

Status: success
API Version: v2
Total users: 2

--- Navkrishna ---
  Email: [email protected]
  Role: ADMIN
  Verified: true
  Plan: PRO
  Price: $199.99
  Features: [analytics, api_access, priority_support]
  GitHub: krrishnaaaa
  Teams: 2
    - Backend (OWNER)
    - Mobile (MEMBER)
  Last login: 2026-03-14T18:45:00Z

--- Priya Sharma ---
  Email: [email protected]
  Role: MEMBER
  Verified: false
  Plan: TRIAL
  Price: $0.0
  Features: [analytics]
  GitHub: priya-dev
  Teams: 0
  Last login: null

Same output, a fraction of the code.

Gson + Kotlin: The Null Safety Trap

There’s a well-known gotcha when using Gson with Kotlin. Gson creates objects using Unsafe (or reflection), bypassing the constructor. This means Gson can set a non-nullable Kotlin property to null without the compiler catching it.

data class Example(
  val name: String  // non-nullable
)

// If the JSON has "name": null, Gson will set name to null at runtime.
// Kotlin's type system won't protect you — no NullPointerException at parse time.
// You'll crash later when you try to use `name`.

There are a few ways to handle this:

  1. Make everything nullable and check after parsing — safe but verbose
  2. Use default valuesval name: String = "" — Gson ignores defaults, but at least the constructor won’t fail
  3. Use a Gson deserializer that validates — more work, but robust
  4. Switch to Moshi — it uses Kotlin’s constructor and respects nullability (spoiler for Part 3)

For most projects, option 1 (nullable + validation) or option 4 (use Moshi) is the pragmatic choice.

Comparison with org.json

Whatorg.jsonGson
Data classes89 (added Data wrapper)
Parsing code~80 lines~5 lines
Null handlingManual isNull() checksAutomatic (but see the trap above)
Date handlingManual Instant.parse()Custom TypeAdapter (written once)
Type safetyNone (string keys)Compile-time (mostly)
Dependency sizeBuilt into Android~280 KB

Using in Android

Gson is one of the most popular JSON libraries on Android. Add the Gradle dependency and you’re good to go.

// With Retrofit + Gson converter
val retrofit = Retrofit.Builder()
  .baseUrl("https://api.example.com/")
  .addConverterFactory(GsonConverterFactory.create(
    GsonBuilder()
      .registerTypeAdapter(Instant::class.java, InstantTypeAdapter())
      .create()
  ))
  .build()

If you’re using Retrofit, the converter-gson artifact handles serialization and deserialization automatically — you just define your data classes and Retrofit interface.

What’s Next

In the next post, we’ll parse this same JSON using Moshi — a library built specifically for Kotlin. Moshi respects Kotlin’s null safety, uses the constructor for object creation, and supports code generation to avoid reflection entirely. If you’re starting a new Kotlin project, it’s the library you probably want.