DevToolBox無料
ブログ

JSONからKotlinデータクラスへ:kotlinx.serialization、Gson、Moshiの完全ガイド

11分by DevToolBox

TL;DR

Convert JSON to Kotlin data classes using kotlinx.serialization for multiplatform, Gson for Android simplicity, or Moshi for annotation-based mapping. Data classes provide automatic equals, hashCode, and copy().

Key Takeaways

  • Kotlin data classes auto-generate equals(), hashCode(), toString(), and copy()
  • @Serializable with kotlinx.serialization is the modern multiplatform approach
  • @SerialName maps JSON keys to Kotlin property names
  • Nullable types (String?) handle null JSON values safely
  • Default parameter values let you skip optional JSON fields
  • Sealed classes model discriminated union types from JSON
  • Gson's @SerializedName and Moshi's @Json serve the same purpose as @SerialName

Why Use Kotlin Data Classes for JSON?

Kotlin data classes are purpose-built for holding structured data. Unlike Java POJOs, they eliminate hundreds of lines of boilerplate — equals(), hashCode(), toString(), and copy() are all generated by the compiler.

When deserializing JSON APIs, data classes give you compile-time type safety, IDE autocomplete, and Kotlin null safety — which prevents the null pointer exceptions that plague Java JSON code.

FeatureKotlin data classJava POJO
equals/hashCodeAuto-generatedManual or Lombok
toString()Auto-generatedManual or Lombok
copy()Built-inNot available
DestructuringcomponentN() built-inNot available
Null safetyEnforced by type systemRuntime NPE risk
Immutabilityval by defaultRequires discipline
Lines of code1 line30-50+ lines
Try our free JSON to Kotlin Data Class tool →

Basic Data Class from JSON (kotlinx.serialization)

The modern, Kotlin-first approach uses kotlinx.serialization. It is multiplatform, generates serializers at compile time (no reflection), and integrates with Kotlin null safety.

Given this JSON:

{
  "id": 42,
  "username": "alice",
  "email": "alice@example.com",
  "age": 30,
  "is_active": true,
  "bio": null
}

The Kotlin data class with kotlinx.serialization:

import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName

@Serializable
data class User(
    val id: Int,
    val username: String,
    val email: String,
    val age: Int,
    @SerialName("is_active")
    val isActive: Boolean,
    val bio: String? = null  // nullable + default value
)

// Deserialize
val user = Json.decodeFromString<User>(jsonString)

// Serialize back to JSON
val json = Json.encodeToString(user)

Add to build.gradle.kts:

plugins {
    kotlin("plugin.serialization") version "1.9.22"
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
}

Gson Alternative

Google\u0027s Gson is battle-tested on Android and requires zero code generation. Use it when you need a simple drop-in that works with existing Java libraries. Note that Gson uses reflection and does not understand Kotlin null safety — a null JSON field on a non-nullable Kotlin property causes a runtime crash.

import com.google.gson.annotations.SerializedName

// No @Serializable needed — Gson uses reflection
data class User(
    val id: Int,
    val username: String,
    val email: String,
    val age: Int,
    @SerializedName("is_active")
    val isActive: Boolean,
    val bio: String? = null
)

// Setup
val gson = GsonBuilder()
    .setLenient()
    .create()

// Deserialize
val user = gson.fromJson(jsonString, User::class.java)

// Serialize
val json = gson.toJson(user)
// build.gradle.kts
dependencies {
    implementation("com.google.code.gson:gson:2.10.1")
}

Moshi Alternative

Square\u0027s Moshi is designed with Kotlin in mind and supports both reflection and code generation (kapt/KSP). Unlike Gson, Moshi understands Kotlin null safety and throws JsonDataException for null mismatches instead of letting them pass silently.

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)  // triggers KSP/kapt code generation
data class User(
    val id: Int,
    val username: String,
    val email: String,
    val age: Int,
    @Json(name = "is_active")
    val isActive: Boolean,
    val bio: String? = null
)

// Setup
val moshi = Moshi.Builder()
    .addLast(KotlinJsonAdapterFactory())
    .build()

val adapter = moshi.adapter(User::class.java)

// Deserialize
val user = adapter.fromJson(jsonString)

// Serialize
val json = adapter.toJson(user)
// build.gradle.kts
plugins {
    id("com.google.devtools.ksp") version "1.9.22-1.0.17"
}

dependencies {
    implementation("com.squareup.moshi:moshi:1.15.1")
    implementation("com.squareup.moshi:moshi-kotlin:1.15.1")
    ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.1")
}

Jackson with Kotlin

Jackson is the de-facto standard in Spring Boot projects. Add the jackson-module-kotlin to make it understand Kotlin data classes, nullable types, and default parameters.

import com.fasterxml.jackson.annotation.JsonProperty

// No special annotation needed if jackson-module-kotlin is registered
data class User(
    val id: Int,
    val username: String,
    val email: String,
    val age: Int,
    @JsonProperty("is_active")
    val isActive: Boolean,
    val bio: String? = null
)

// Setup with Kotlin module
val mapper = ObjectMapper().apply {
    registerModule(KotlinModule.Builder().build())
    configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
}

// Deserialize
val user = mapper.readValue<User>(jsonString)

// Serialize
val json = mapper.writeValueAsString(user)
// build.gradle.kts (Spring Boot adds these automatically)
dependencies {
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.16.1")
    implementation("com.fasterxml.jackson.core:jackson-databind:2.16.1")
}

Nested Objects and Lists

JSON with nested objects maps to separate data classes. Arrays map to List<T>.

// Input JSON
{
  "order_id": "ORD-001",
  "customer": {
    "id": 42,
    "name": "Alice"
  },
  "items": [
    { "sku": "ITEM-A", "quantity": 2, "price": 9.99 },
    { "sku": "ITEM-B", "quantity": 1, "price": 24.99 }
  ],
  "tags": ["priority", "gift"]
}
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName

@Serializable
data class Customer(
    val id: Int,
    val name: String
)

@Serializable
data class OrderItem(
    val sku: String,
    val quantity: Int,
    val price: Double
)

@Serializable
data class Order(
    @SerialName("order_id") val orderId: String,
    val customer: Customer,
    val items: List<OrderItem>,
    val tags: List<String> = emptyList()
)

// Parse entire hierarchy in one call
val order = Json.decodeFromString<Order>(jsonString)

// Access nested data
println(order.customer.name)       // Alice
println(order.items[0].sku)        // ITEM-A
println(order.items.sumOf { it.price * it.quantity })  // total cost

Nullable Fields and Default Values

Kotlin\u0027s type system distinguishes nullable (String?) from non-nullable (String) types. Map JSON null values to nullable types, and use default values for optional JSON keys that may be absent entirely.

@Serializable
data class UserProfile(
    val id: Int,                           // required, never null
    val username: String,                  // required, never null
    val bio: String? = null,               // JSON null or absent -> null
    val avatarUrl: String? = null,         // JSON null or absent -> null
    val followersCount: Int = 0,           // absent in JSON -> defaults to 0
    val isVerified: Boolean = false,       // absent in JSON -> defaults to false
    val roles: List<String> = emptyList(), // absent in JSON -> empty list
    val metadata: Map<String, String> = emptyMap()
)

// Lenient parsing — ignores extra keys, uses defaults for missing keys
val json = Json {
    ignoreUnknownKeys = true
    coerceInputValues = true  // coerces invalid values to defaults
}

// Null-safe access
val profile = json.decodeFromString<UserProfile>(jsonString)
val displayBio = profile.bio ?: "No bio provided"
profile.avatarUrl?.let { loadAvatar(it) }  // safe call

With ignoreUnknownKeys = true, the parser silently ignores JSON fields that have no matching property in the data class — essential for robust API clients that must handle evolving APIs.

Enum Handling

JSON string values that represent a finite set of options map naturally to Kotlin enums. Both kotlinx.serialization and Gson support enum serialization out of the box.

import kotlinx.serialization.SerialName

@Serializable
enum class OrderStatus {
    @SerialName("pending") PENDING,
    @SerialName("processing") PROCESSING,
    @SerialName("shipped") SHIPPED,
    @SerialName("delivered") DELIVERED,
    @SerialName("cancelled") CANCELLED,
}

@Serializable
data class Order(
    val id: String,
    val status: OrderStatus,  // maps JSON "shipped" to OrderStatus.SHIPPED
    val amount: Double
)

// Usage
val order = Json.decodeFromString<Order>("""
    {"id": "ORD-1", "status": "shipped", "amount": 49.99}
""")
println(order.status)  // SHIPPED

// Unknown enum values — use lenient mode or provide a fallback
val lenientJson = Json {
    ignoreUnknownKeys = true
    isLenient = true
}

// With Gson — enums serialize by name automatically
val gson = Gson()
val json = gson.toJson(OrderStatus.SHIPPED)  // "SHIPPED"

Date/Time with kotlinx.serialization

kotlinx-datetime provides multiplatform date/time classes. Use Instant for timestamps (ISO 8601), LocalDate for dates without time.

import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.serializers.InstantIso8601Serializer

@Serializable
data class Event(
    val id: Int,
    val title: String,
    // ISO 8601 string in JSON: "2026-02-27T10:30:00Z"
    @Serializable(with = InstantIso8601Serializer::class)
    val createdAt: Instant,
    // Date string in JSON: "2026-03-01"
    val eventDate: LocalDate,
)

// build.gradle.kts
// implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")

// For JVM-only (Gson/Jackson) with java.time
data class EventJvm(
    val id: Int,
    @JsonAdapter(InstantAdapter::class)  // Gson custom adapter
    val createdAt: java.time.Instant,
)

// Custom Gson TypeAdapter for Instant
class InstantAdapter : TypeAdapter<java.time.Instant>() {
    override fun write(out: JsonWriter, value: java.time.Instant) {
        out.value(value.toString())
    }
    override fun read(`in`: JsonReader): java.time.Instant {
        return java.time.Instant.parse(`in`.nextString())
    }
}

Sealed Classes for Union Types

Sealed classes model JSON payloads that have different shapes based on a discriminator field. kotlinx.serialization supports polymorphism through SerializersModule.

// JSON variants:
// {"type": "card", "last4": "1234", "brand": "Visa"}
// {"type": "paypal", "email": "user@example.com"}
// {"type": "bank", "account_number": "****5678"}

@Serializable
sealed class PaymentMethod {
    abstract val type: String
}

@Serializable
@SerialName("card")
data class CardPayment(
    override val type: String = "card",
    val last4: String,
    val brand: String
) : PaymentMethod()

@Serializable
@SerialName("paypal")
data class PayPalPayment(
    override val type: String = "paypal",
    val email: String
) : PaymentMethod()

@Serializable
@SerialName("bank")
data class BankPayment(
    override val type: String = "bank",
    @SerialName("account_number") val accountNumber: String
) : PaymentMethod()

// Configure polymorphic serialization
val json = Json {
    serializersModule = SerializersModule {
        polymorphic(PaymentMethod::class) {
            subclass(CardPayment::class)
            subclass(PayPalPayment::class)
            subclass(BankPayment::class)
        }
    }
}

// Parse — Kotlin when expression gives exhaustive handling
val payment = json.decodeFromString<PaymentMethod>(jsonString)
when (payment) {
    is CardPayment  -> println("Card: ${payment.brand} *${payment.last4}")
    is PayPalPayment -> println("PayPal: ${payment.email}")
    is BankPayment  -> println("Bank: ${payment.accountNumber}")
}

Coroutines + Ktor Client Integration

Ktor is the Kotlin-native HTTP client that pairs naturally with kotlinx.serialization. It handles JSON content negotiation, request/response serialization, and coroutine integration.

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*

@Serializable
data class GitHubUser(
    val login: String,
    val id: Int,
    val name: String?,
    @SerialName("public_repos") val publicRepos: Int,
    val bio: String? = null
)

// Create Ktor client with kotlinx.serialization
val client = HttpClient(CIO) {
    install(ContentNegotiation) {
        json(Json {
            ignoreUnknownKeys = true
            isLenient = true
        })
    }
}

// Suspend function — call from coroutine scope
suspend fun fetchUser(username: String): GitHubUser {
    return client.get("https://api.github.com/users/$username") {
        header("Accept", "application/vnd.github.v3+json")
    }.body()
}

// Usage in coroutine
viewModelScope.launch {
    val user = fetchUser("octocat")
    println(user.name)         // The Octocat
    println(user.publicRepos)  // number of public repos
}

// POST with serialized body
suspend fun createUser(user: User): User {
    return client.post("https://api.example.com/users") {
        contentType(ContentType.Application.Json)
        setBody(user)
    }.body()
}
// build.gradle.kts
dependencies {
    implementation("io.ktor:ktor-client-core:2.3.9")
    implementation("io.ktor:ktor-client-cio:2.3.9")
    implementation("io.ktor:ktor-client-content-negotiation:2.3.9")
    implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.9")
}

Android / Retrofit Integration

Retrofit is the dominant Android HTTP client. Combine it with either Gson (traditional) or kotlinx.serialization (modern) converter factories.

// Data classes (same structure works with both converters)
@Serializable
data class Post(
    val id: Int,
    val title: String,
    val body: String,
    @SerialName("user_id") val userId: Int
)

// Retrofit interface
interface PostsApi {
    @GET("posts")
    suspend fun getPosts(): List<Post>

    @GET("posts/{id}")
    suspend fun getPost(@Path("id") id: Int): Post

    @POST("posts")
    suspend fun createPost(@Body post: Post): Post
}

// Option A: kotlinx.serialization converter (recommended)
val retrofit = Retrofit.Builder()
    .baseUrl("https://jsonplaceholder.typicode.com/")
    .addConverterFactory(
        Json { ignoreUnknownKeys = true }
            .asConverterFactory("application/json".toMediaType())
    )
    .build()

// Option B: Gson converter (simpler setup)
val retrofit = Retrofit.Builder()
    .baseUrl("https://jsonplaceholder.typicode.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()

val api = retrofit.create(PostsApi::class.java)

// Usage in ViewModel
viewModelScope.launch {
    try {
        val posts = api.getPosts()  // returns List<Post>
        _uiState.value = UiState.Success(posts)
    } catch (e: Exception) {
        _uiState.value = UiState.Error(e.message ?: "Unknown error")
    }
}
// build.gradle.kts (Android)
dependencies {
    // Retrofit
    implementation("com.squareup.retrofit2:retrofit:2.9.0")

    // Option A: kotlinx.serialization converter
    implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")

    // Option B: Gson converter
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")

    // Coroutines for Android
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}

Tools for JSON to Kotlin Conversion

Several tools automate Kotlin data class generation from JSON — saving time and preventing typos:

ToolBest forLibrary support
DevToolBox JSON to KotlinOnline, quick usekotlinx.serialization, Gson, Moshi
IntelliJ IDEA pluginIDE integrationGson, Jackson, kotlinx
JsonToKotlinClass pluginComplex nested JSONAll major libraries
json-to-kotlin.comBrowser-basedkotlinx.serialization
Moshi Kotlin codegenProduction AndroidMoshi only

The DevToolBox JSON to Kotlin converter supports all three major libraries — paste your JSON and instantly get annotated data classes with proper @SerialName, @SerializedName, or @Json(name) annotations for snake_case fields.

Try our free JSON to Kotlin Data Class tool →

Common Pitfalls

Avoid these common mistakes when working with JSON and Kotlin data classes:

1. Forgetting @Serializable on nested classes

Every class in the hierarchy needs @Serializable with kotlinx.serialization — not just the root class. Missing annotations cause SerializationException at runtime.

// Wrong
data class Address(val city: String)  // missing @Serializable

// Correct
@Serializable
data class Address(val city: String)
2. Non-nullable type receiving JSON null

If JSON has null but your Kotlin type is non-nullable, kotlinx.serialization throws SerializationException. Gson silently passes null and causes NPE later.

// Dangerous with Gson
data class User(val name: String)  // crash if JSON has "name": null

// Safe
data class User(val name: String? = null)
3. Missing ignoreUnknownKeys

By default, kotlinx.serialization throws on unknown JSON keys. Set ignoreUnknownKeys = true when consuming external APIs that may add new fields.

val json = Json { ignoreUnknownKeys = true }
4. Forgetting to register KotlinModule with Jackson

Without KotlinModule, Jackson cannot handle Kotlin default parameters, null safety, or data class constructors correctly.

val mapper = ObjectMapper().registerModule(KotlinModule.Builder().build())
5. Using data class with Room as an @Embedded vs @TypeConverter

Room cannot serialize complex nested objects to columns automatically. Use @Embedded for flat objects or @TypeConverter for List, Map, and custom types.

@Entity
data class UserEntity(
    @PrimaryKey val id: Int,
    @Embedded val address: Address,  // flattened to columns
    @TypeConverters val tags: List<String>  // needs TypeConverter
)
6. Snake_case mismatch

JSON APIs typically use snake_case but Kotlin convention is camelCase. Always use @SerialName (kotlinx), @SerializedName (Gson), or @Json(name) (Moshi) for key mapping.

// JSON: {"user_name": "alice"}
@Serializable
data class User(
    @SerialName("user_name") val userName: String  // correct mapping
)

Library Comparison Summary

Criterionkotlinx.serializationGsonMoshiJackson
Multiplatform (KMP)YesJVM onlyJVM onlyJVM only
Kotlin null safetyFull supportPartialFull supportWith module
ReflectionNo (compile-time)YesOptional (KSP)Yes
Setup complexityMediumSimpleMediumMedium
Spring BootSupportedSupportedSupportedDefault
Android RetrofitSupportedTraditionalSupportedSupported
PerformanceExcellentGoodExcellentGood
RecommendationNew KMP/Ktor projectsSimple AndroidAndroid with codegenSpring Boot

Frequently Asked Questions

Q: What's the difference between data class and regular class for JSON?

A: Data classes auto-generate structural equality (equals/hashCode), copy(), toString(), and destructuring declarations. Regular classes require manual implementation of these. For JSON deserialization, data classes work identically — the real benefit is the generated boilerplate. However, data classes require at least one constructor parameter, making them ideal for immutable value objects that map cleanly to JSON payloads.

Q: Should I use kotlinx.serialization or Gson?

A: kotlinx.serialization is recommended for new projects — it is multiplatform (works on JVM, JS, Native), null-safe, and integrates deeply with Kotlin type system. It generates serializers at compile time, avoiding reflection. Gson is mature and simpler for Android-only projects but uses reflection and has quirks with Kotlin nullability. For new Kotlin Multiplatform (KMP) or Ktor projects, always choose kotlinx.serialization.

Q: How do I handle snake_case JSON keys in Kotlin?

A: Use @SerialName("snake_key") for kotlinx.serialization to map JSON keys to idiomatic Kotlin camelCase property names. For Gson, use @SerializedName("snake_key"). For Moshi, use @Json(name = "snake_key"). Alternatively, kotlinx.serialization supports a global NamingStrategy via JsonBuilder: Json { namingStrategy = JsonNamingStrategy.SnakeCase } to automatically convert all camelCase properties.

Q: How do nullable fields work in Kotlin JSON parsing?

A: Declare fields as String? (nullable type) — they accept null JSON values. With kotlinx.serialization, a non-nullable field that receives JSON null will throw SerializationException. With Gson, null JSON values on non-nullable Kotlin types can cause NullPointerExceptions at runtime. Always mark optional JSON fields as nullable (String?) or provide default values (val name: String = "") to avoid crashes.

Q: Can I use data classes with Room database?

A: Yes — annotate the data class with @Entity and Room treats it as a database entity. For complex nested types that are not primitive, use @TypeConverter to convert them to/from a storable type (usually JSON string). Example: convert a List<String> to a JSON string using Gson or kotlinx.serialization, then store it as a TEXT column. Data class properties annotated with @Embedded map nested objects to columns.

Q: How do I parse JSON arrays in Kotlin?

A: Use List<T> — kotlinx.serialization handles it automatically. Example: val users = Json.decodeFromString<List<User>>(jsonString). For Gson: val users = gson.fromJson(jsonString, object : TypeToken<List<User>>() {}.type). For Moshi: val adapter = moshi.adapter<List<User>>(Types.newParameterizedType(List::class.java, User::class.java)) then adapter.fromJson(jsonString). Nested arrays become List<List<T>>.

Q: What are sealed classes for in JSON?

A: Sealed classes model discriminated unions where JSON has different shapes based on a "type" field. For example, a payment JSON might be {"type":"card",...} or {"type":"paypal",...}. With kotlinx.serialization, use @Serializable with polymorphism: add subclasses with SerializersModule and PolymorphicSerializer. Each subclass handles a different JSON shape, and you switch on the type discriminator to parse the correct variant.

Q: How do I use default values for missing JSON fields?

A: Add default values in the constructor: val count: Int = 0 or val tags: List<String> = emptyList(). With kotlinx.serialization, you must also set Json { ignoreUnknownKeys = true } and the library will use defaults for missing keys. Gson also respects Kotlin default values when used with GsonBuilder and properly configured. Default values prevent crashes when the API evolves and adds or removes fields.

Ready to convert your JSON to Kotlin data classes?

Try our free JSON to Kotlin Data Class tool →
𝕏 Twitterin LinkedIn
この記事は役に立ちましたか?

最新情報を受け取る

毎週の開発ヒントと新ツール情報。

スパムなし。いつでも解除可能。

Try These Related Tools

KTJSON to Kotlin{ }JSON FormatterTSJSON to TypeScriptJVJSON to Java Class

Related Articles

JSONをJavaクラスに変換:JacksonでのPOJO生成完全ガイド

Jackson、Lombok、GsonでJSONをJavaクラス、POJO、Recordに変換する方法を学びます。アノテーション、ネストオブジェクト、ジェネリクスを網羅。

JSON to TypeScript オンラインガイド:開発者完全マニュアル

JSONからTypeScript型を自動生成する方法。interface vs type、オプショナル/null許容フィールド、ネストオブジェクト、ユニオン型、Zodランタイム検証、ジェネリックAPIレスポンス型、tsconfigベストプラクティス。