Free & open source — no account required

v1.2.5-PRICING-19
Web & Frontend • Engineering Documentation

Kotlin Mastery: Mastering JSON-to-Data-Class Generation

This technical guide provides an in-depth analysis of the json to kotlin class engine, best practices for implementation, and data security standards.

JSON to Kotlin Data Class: Serialization with kotlinx.serialization and Moshi

Kotlin's data classes are the natural fit for JSON mapping — they come with generated equals(), hashCode(), copy(), and toString() built into the language. Converting your JSON to Kotlin data classes unlocks two dominant serialization libraries: kotlinx.serialization (Kotlin-first, multiplatform-ready, compile-time code generation) and Moshi (Square's reflection-based library with Kotlin codegen adapter). For Android development, Retrofit + Moshi or Retrofit + kotlinx.serialization is the standard stack. For backend Kotlin with Ktor or Spring Boot, kotlinx.serialization integrates natively.

Live Example: Order Model with kotlinx.serialization

// Input JSON
{
  "order_id": "ORD-772",
  "status": "shipped",
  "customer_name": "Sarah Jenkins",
  "is_gift": false,
  "items": [
    { "sku": "WGT-001", "quantity": 2, "unit_price": 49.99 }
  ],
  "created_at": "2024-01-15T08:30:00Z"
}

// Generated Kotlin Data Classes
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName

@Serializable
data class OrderItem(
    val sku: String,
    val quantity: Int,
    @SerialName("unit_price")
    val unitPrice: Double
)

@Serializable
data class Order(
    @SerialName("order_id")
    val orderId: String,
    val status: OrderStatus,
    @SerialName("customer_name")
    val customerName: String,
    @SerialName("is_gift")
    val isGift: Boolean,
    val items: List<OrderItem>,
    @SerialName("created_at")
    val createdAt: String  // ISO 8601 string — use kotlinx-datetime for parsing
)

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

// Deserialize
import kotlinx.serialization.json.Json

val json = Json { ignoreUnknownKeys = true }
val order: Order = json.decodeFromString(jsonString)

// Serialize
val jsonOut: String = json.encodeToString(order)

@SerialName("snake_case_key") maps the JSON key to a Kotlin property name following Kotlin's camelCase convention. Using ignoreUnknownKeys = true in the Json config makes the model tolerant of extra fields in the API response — essential when consuming APIs that evolve over time.

Nullable and Optional Fields

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

@Serializable
data class UserProfile(
    val id: String,
    val username: String,
    val email: String,

    // Nullable — field is present but may be null
    val bio: String? = null,

    // Optional with default — field may be absent from JSON
    val role: String = "user",

    // Nullable optional — absent OR null → null
    val deletedAt: String? = null,

    // Nested nullable object
    val address: Address? = null,
)

@Serializable
data class Address(
    val street: String,
    val city: String,
    val country: String,
    val postalCode: String? = null,
)

In Kotlin + kotlinx.serialization, a field with a default value is optional in JSON — if the key is absent, the default is used. A nullable type (T?) without a default requires the key to be present (even if null). To handle both absent and null, use val field: T? = null.

Custom Serializers

import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.datetime.Instant

// Custom serializer for kotlinx-datetime Instant
object InstantSerializer : KSerializer<Instant> {
    override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: Instant) {
        encoder.encodeString(value.toString())
    }

    override fun deserialize(decoder: Decoder): Instant {
        return Instant.parse(decoder.decodeString())
    }
}

@Serializable
data class Event(
    val id: String,
    val name: String,
    @Serializable(with = InstantSerializer::class)
    val occurredAt: Instant,
)

// Custom serializer for sealed classes (discriminated unions)
@Serializable
sealed class ApiEvent {
    @Serializable
    @SerialName("click")
    data class Click(val x: Int, val y: Int) : ApiEvent()

    @Serializable
    @SerialName("keypress")
    data class Keypress(val key: String, val ctrl: Boolean) : ApiEvent()
}

// Deserializes {"type": "click", "x": 100, "y": 200} automatically
// kotlinx.serialization reads the "type" field as discriminator

Moshi with Kotlin Codegen

// build.gradle.kts (app)
dependencies {
    implementation("com.squareup.moshi:moshi:1.15.0")
    implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
    ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")
}

// Model with Moshi
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)  // triggers codegen
data class Product(
    val id: String,
    val name: String,
    @Json(name = "unit_price")
    val unitPrice: Double,
    val inStock: Boolean,
    val tags: List<String> = emptyList(),
)

// Moshi instance with Kotlin adapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory

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

val adapter = moshi.adapter(Product::class.java)
val product: Product? = adapter.fromJson(jsonString)
val json: String = adapter.toJson(product)

Retrofit Integration for Android

// API interface with kotlinx.serialization converter
import retrofit2.Retrofit
import retrofit2.http.*
import kotlinx.serialization.Serializable
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import okhttp3.MediaType.Companion.toMediaType

@Serializable
data class UserResponse(
    val id: String,
    val username: String,
    val email: String,
)

interface UserApi {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: String): UserResponse

    @POST("users")
    suspend fun createUser(@Body request: CreateUserRequest): UserResponse

    @GET("users")
    suspend fun listUsers(
        @Query("page") page: Int = 1,
        @Query("limit") limit: Int = 20,
    ): List<UserResponse>
}

// Retrofit setup
val json = Json { ignoreUnknownKeys = true; isLenient = true }
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
    .build()

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

Ktor Backend Integration

// Ktor server with kotlinx.serialization
import io.ktor.server.application.*
import io.ktor.server.routing.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.plugins.contentnegotiation.*

fun Application.configureRouting() {
    install(ContentNegotiation) {
        json(Json { prettyPrint = false; ignoreUnknownKeys = true })
    }

    routing {
        post("/users") {
            val request = call.receive<CreateUserRequest>()  // auto-deserialized
            val user = userService.create(request)
            call.respond(user)  // auto-serialized
        }
    }
}

Best Practices for Production

  • Use ignoreUnknownKeys = true globally: Set this on your Json instance. API responses evolve — unknown fields should not crash your app.
  • Prefer data classes over regular classes for JSON models: Data classes get structural equality for free. Two Order instances with the same field values are equal with ==, which is critical for correct state management in Android ViewModels.
  • Use sealed classes for discriminated unions: sealed class ApiEvent with @SerialName on each subclass provides exhaustive when expression handling — the compiler forces you to handle every event type.
  • Use copy() for immutable updates: Never mutate JSON model fields (use val). Create updated versions with order.copy(status = OrderStatus.DELIVERED) — this is the idiomatic Kotlin pattern for state updates.

FAQ

Q: kotlinx.serialization vs. Moshi — which should I use?
A: kotlinx.serialization is the recommended choice for new projects. It supports Kotlin Multiplatform (shared code across Android/iOS/JVM), uses compile-time code generation (no reflection at runtime), and integrates natively with Ktor. Moshi is excellent for Android-only projects or teams already invested in Square's OkHttp/Retrofit ecosystem — its codegen adapter is well-tested and battle-hardened.

Q: How do I parse ISO 8601 dates in Kotlin?
A: Use kotlinx-datetime (implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")) for multiplatform datetime types. Register a custom serializer or use @Contextual with a serializer registered on the Json instance. For JVM-only, java.time.Instant with a custom serializer works too.

Q: What is the Kotlin equivalent of TypeScript's Record<string, unknown>?
A: Use Map<String, Any?> with JsonObject from kotlinx.serialization for structured access, or kotlinx.serialization.json.JsonElement for a typed JSON tree that can be traversed without a concrete data class.

Q: How do I handle a JSON field named the same as a Kotlin keyword?
A: Use @SerialName("type") with a renamed property: @SerialName("type") val kind: String. The JSON key is "type"; the Kotlin property is kind.

Developer FAQ

Is the processing local-only?

Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.

Can I use this for enterprise projects?

Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.