Why generate Kotlin data classes from a sample?
A Kotlin data class is a one-line promise — and JSON breaks
promises for a living. The key that is missing from the
tenth element, the value that is sometimes null, the
snake_case naming your backend team swears by: each one has
a precise Kotlin spelling (a nullable type with a null
default, an @SerialName pin), and getting them
wrong surfaces as a deserialization exception in
production, not a compile error at your desk. The sample
already contains the evidence for every one of those
decisions. This converter reads it off mechanically:
objects become data classes, arrays become
List<T>, absence becomes nullability
with a default, renames get their annotations — and the
annotation set is only emitted when the sample actually
needs it.
How to use this tool
- Paste the JSON. A real API response is ideal; include several array elements if you have them, because absence across elements is how nullable properties are discovered.
- Name the root class.
Rootis fine for a quick look; for code you will keep, use the name it will wear in your project. - Press Generate Kotlin. The root class comes first, the classes it references after it.
- Read the notes if any appear. They list every
Anyslot, every keyword written in backticks and every name collision — nothing was decided silently. - Copy or download the result, then refine it: narrow
Longwhere your domain knows better, and write the custom serializer for anyAnyslot you keep.
The rules it follows
Numbers. Whole numbers become
Long; any fraction widens the slot to
Double — a JSON 1.0 is the number
1 to every parser, so a field your API always sends as
2.0 reads as an integer sample unless a real
fraction appears. Names. Classes are
PascalCase after the key that holds them; properties are
camelCase, with @SerialName pinning the
original key whenever camelCasing changes the spelling, and
keywords written in backticks. When nothing needs a pin,
no annotations or imports are emitted at all.
Absence and null. Missing from some
elements or sometimes null → a nullable type with
= null. Null everywhere → Any?;
genuinely mixed types → Any; both always
noted. Merging. Identical shapes — same
keys, same types — share one class instead of being
emitted twice. An object with no keys at all becomes a
plain class, noted, because a data class
needs at least one property.
Worked example
Given this sample (the second user has no nickname, and the keys are already camelCase):
{
"teamName": "Platform",
"users": [
{ "id": 1, "firstName": "Ada", "nickname": "ada" },
{ "id": 2, "firstName": "Grace" }
]
} the converter emits — note that no imports are needed, because no name had to change:
data class Root(
val teamName: String,
val users: List<User>
)
data class User(
val id: Long,
val firstName: String,
val nickname: String? = null
)
Had any key been snake_case, every class would have gained
@Serializable, the renamed property an
@SerialName pin you can read right in the
output, and the file its two imports — the machinery
appears exactly when the sample first needs it.