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:
@SerializedNameis 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 tonullwhen the JSON value isnull. But be careful: Gson can also set non-nullable Kotlin properties tonullat runtime since it bypasses the constructor. More on that below. - We added a
Dataclass — unlike theorg.jsonversion where we manually unwrappeddata.users, Gson maps the full structure, so we need a class for thedatawrapper.
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:
- Make everything nullable and check after parsing — safe but verbose
- Use default values —
val name: String = ""— Gson ignores defaults, but at least the constructor won’t fail - Use a Gson deserializer that validates — more work, but robust
- 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
| What | org.json | Gson |
|---|---|---|
| Data classes | 8 | 9 (added Data wrapper) |
| Parsing code | ~80 lines | ~5 lines |
| Null handling | Manual isNull() checks | Automatic (but see the trap above) |
| Date handling | Manual Instant.parse() | Custom TypeAdapter (written once) |
| Type safety | None (string keys) | Compile-time (mostly) |
| Dependency size | Built 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.