Boneyard Tools

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.

Frequently asked questions

Should I use val or var in the data classes?

The generator emits val, which makes each instance read only and safe to share. Switch a property to var only when you genuinely need to mutate it after decoding.

How do I model a field that is sometimes missing?

Give the property a nullable type and a default, such as val note: String? = null. With kotlinx.serialization that lets the decoder skip the field cleanly instead of throwing.