Turning JSON API responses into Kotlin data classes
Why data classes fit JSON payloads, how types are guessed from one sample, and how to add kotlinx.serialization and correct nullability.
What a data class gives you for free
A Kotlin data class is built for holding structured values exactly like a JSON payload. Declaring the class with val properties makes the compiler generate equals, hashCode and toString, plus a copy function and componentN destructuring. That means two decoded responses with the same field values compare as equal, log in a readable form, and can be copied with a single field changed. For a mobile or server client that maps every endpoint to a class, this removes a large amount of boilerplate you would otherwise write and maintain by hand.
Inference from a single sample has limits
The generator looks at one example object and picks the narrowest type that fits each value. That is a fast start, but a single sample cannot tell you which fields are optional, which are sometimes null, or which arrays are sometimes empty. A field that shows 1 here might be a decimal elsewhere and belongs as Double. Treat the generated classes as a first draft, then reconcile them against the API documentation before you ship.
Adding serialization and mapping keys
The output is deliberately annotation free so you can choose a library. With kotlinx.serialization you add the kotlinx-serialization-json dependency, mark each class @Serializable, and decode with Json.decodeFromString. When a JSON key uses snake_case, keep an idiomatic camelCase property and bridge the two with a @SerialName annotation. Moshi and Gson follow similar patterns with @Json and @SerializedName, so the plain classes stay portable across all three.
Getting nullability right
Nullability is where a hand review pays off most. A field that arrives as null in the sample is typed Any?, which is rarely what you want long term, so widen it to a concrete nullable type such as String? once you know the shape. Fields that can be absent entirely, rather than present and null, should carry a Kotlin default so decoding does not fail. Spending a few minutes here prevents runtime crashes that a too optimistic model would only reveal in production.