PCSalt
YouTube GitHub
Back to Kotlin
Kotlin · 3 min read

JSON Parsing in Kotlin — Parsing with Moshi

Parse a complex JSON response using Moshi with Kotlin data classes, codegen, and proper null safety. The recommended JSON library for modern Android.


This is Part 3 of the JSON Parsing in Kotlin series. In Part 1, we parsed everything by hand. In Part 2, we let Gson handle the mapping — but we ran into Kotlin null safety issues. Now let’s use Moshi, a library that was built with Kotlin in mind.

Looking for the Java version? Read it here.

The JSON

Same JSON as the previous posts — 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

Moshi has two Kotlin integration options: reflection-based (moshi-kotlin) and codegen (moshi-kotlin-codegen). We’ll use codegen — it generates adapters at compile time, so there’s no reflection overhead at runtime.

Maven:

<dependency>
    <groupId>com.squareup.moshi</groupId>
    <artifactId>moshi</artifactId>
    <version>1.15.1</version>
</dependency>
<dependency>
    <groupId>com.squareup.moshi</groupId>
    <artifactId>moshi-kotlin-codegen</artifactId>
    <version>1.15.1</version>
    <scope>provided</scope>
</dependency>

Gradle (with KSP):

plugins {
  id 'com.google.devtools.ksp' version '2.1.10-1.0.29'
}

dependencies {
  implementation 'com.squareup.moshi:moshi:1.15.1'
  ksp 'com.squareup.moshi:moshi-kotlin-codegen:1.15.1'
}

Note: Moshi codegen uses KSP (Kotlin Symbol Processing) to generate adapter classes at compile time. This means zero reflection at runtime — fast and ProGuard-friendly.

Model Classes

Each data class gets @JsonClass(generateAdapter = true), which tells Moshi’s codegen to generate a JsonAdapter for it. Use @Json(name = "...") where the JSON key doesn’t match the property name.

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.time.Instant

@JsonClass(generateAdapter = true)
data class Social(
  val github: String?,
  val twitter: String?,
  val linkedin: String?
)

@JsonClass(generateAdapter = true)
data class Profile(
  val avatar: String?,
  val bio: String?,
  val social: Social
)

@JsonClass(generateAdapter = true)
data class Subscription(
  val plan: String,
  @Json(name = "billingCycle")
  val billingCycle: String?,
  val price: Double,
  val features: List<String>,
  @Json(name = "trialEndsAt")
  val trialEndsAt: Instant?
)

@JsonClass(generateAdapter = true)
data class Team(
  val id: String,
  val name: String,
  val role: String,
  @Json(name = "memberCount")
  val memberCount: Int
)

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

@JsonClass(generateAdapter = true)
data class Pagination(
  val page: Int,
  @Json(name = "perPage")
  val perPage: Int,
  @Json(name = "totalItems")
  val totalItems: Int,
  @Json(name = "totalPages")
  val totalPages: Int,
  @Json(name = "hasNext")
  val hasNext: Boolean
)

@JsonClass(generateAdapter = true)
data class Meta(
  @Json(name = "requestId")
  val requestId: String,
  val timestamp: Instant,
  @Json(name = "apiVersion")
  val apiVersion: String
)

@JsonClass(generateAdapter = true)
data class Data(
  val users: List<User>,
  val pagination: Pagination
)

@JsonClass(generateAdapter = true)
data class ApiResponse(
  val status: String,
  val data: Data,
  val meta: Meta
)

The big difference from Gson: Moshi’s codegen generates adapters that use Kotlin’s constructor to create objects. This means:

  • If the JSON has null for a non-nullable property, Moshi throws a JsonDataException at parse time — not a random NullPointerException later
  • Default values in the constructor actually work
  • You get proper Kotlin null safety, not just at compile time but at runtime too

Custom Adapter for Instant

Like Gson, Moshi doesn’t handle java.time.Instant out of the box. We write a custom adapter.

import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import java.time.Instant

object InstantAdapter {

  @FromJson
  fun fromJson(value: String?): Instant? =
    value?.let { Instant.parse(it) }

  @ToJson
  fun toJson(value: Instant?): String? =
    value?.toString()
}

Notice how much simpler this is compared to Gson’s TypeAdapter. No JsonReader/JsonWriter, no peek() for null checks — just annotated functions. Moshi handles null delegation automatically.

Parsing

import com.squareup.moshi.Moshi

object MoshiParser {

  private val moshi: Moshi = Moshi.Builder()
    .add(InstantAdapter)
    .build()

  private val adapter = moshi.adapter(ApiResponse::class.java)

  fun parse(jsonString: String): ApiResponse {
    return adapter.fromJson(jsonString)
      ?: throw IllegalStateException("Failed to parse JSON")
  }
}

The adapter.fromJson() returns ApiResponse? (nullable), so we handle the null case explicitly. In practice, this only returns null if the entire JSON input is the literal null.

Putting It Together

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

fun main() {
  val json = Path.of("response.json").readText()
  val response = MoshiParser.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

Why Moshi Over Gson for Kotlin

FeatureGsonMoshi (codegen)
Kotlin null safetyBypasses it (uses Unsafe)Respects it (uses constructor)
Default valuesIgnoredSupported
Code generationNoYes (via KSP)
Reflection at runtimeYesNo (with codegen)
Custom adaptersVerbose TypeAdapterSimple @FromJson/@ToJson
Fail-fast on bad dataNo (silently null)Yes (JsonDataException)
ProGuard/R8 friendlyNeeds rulesCodegen = no reflection = no rules

The bottom line: Moshi was designed for Kotlin. Gson was designed for Java and works with Kotlin — but the cracks show around null safety and default values.

Comparison Across All Three

Whatorg.jsonGsonMoshi
Parsing code~80 lines~5 lines~5 lines
Null handlingManual isNull()Automatic (but unsafe)Automatic + null safe
Date handlingManualCustom TypeAdapterCustom adapter (simpler)
ReflectionNoneYesNo (with codegen)
Android recommendedBuilt-in but verbosePopular but agingRecommended

Using in Android

Moshi is the recommended JSON library for modern Android development. It pairs perfectly with Retrofit and OkHttp.

plugins {
  id 'com.google.devtools.ksp' version '2.1.10-1.0.29'
}

dependencies {
  implementation 'com.squareup.moshi:moshi:1.15.1'
  ksp 'com.squareup.moshi:moshi-kotlin-codegen:1.15.1'
  implementation 'com.squareup.retrofit2:converter-moshi:2.11.0'
}
// With Retrofit + Moshi converter
val moshi = Moshi.Builder()
  .add(InstantAdapter)
  .build()

val retrofit = Retrofit.Builder()
  .baseUrl("https://api.example.com/")
  .addConverterFactory(MoshiConverterFactory.create(moshi))
  .build()

Since Moshi codegen produces adapters at compile time, there’s no reflection at runtime — which means faster startup, smaller APK (no kotlin-reflect), and no ProGuard rules needed for your model classes.

What’s Next

This wraps up the JSON Parsing in Kotlin series. We started with the pain of manual parsing (org.json), moved to automatic mapping with Gson (convenient but leaky null safety), and landed on Moshi (Kotlin-native, null safe, codegen). If you’re building a Kotlin or Android project today, Moshi with codegen is the way to go.

Want to see how these same approaches look in Java? Check out the JSON Parsing in Java series.