JSON to Kotlin

Generate Kotlin data classes from a JSON sample — kotlinx.serialization style with @SerialName pins (and @Serializable plus the imports) only where a key's spelling had to change, nullable types with null defaults where keys come and go or turn null, and Any reported wherever the sample could not settle a type. Free, no sign-up — runs locally in your browser.

Your work is auto-saved in this browser only, so a refresh won't lose it. Nothing is uploaded.

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

  1. 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.
  2. Name the root class. Root is fine for a quick look; for code you will keep, use the name it will wear in your project.
  3. Press Generate Kotlin. The root class comes first, the classes it references after it.
  4. Read the notes if any appear. They list every Any slot, every keyword written in backticks and every name collision — nothing was decided silently.
  5. Copy or download the result, then refine it: narrow Long where your domain knows better, and write the custom serializer for any Any slot 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.

Frequently asked questions

Which serialization library are the classes written for?

kotlinx.serialization — the data classes are in its idiom: @Serializable on the classes and @SerialName wherever a property's Kotlin name differs from its JSON key. Moshi and Gson read data classes happily too, but their rename annotations differ (@Json(name = …) for Moshi); the shapes and nullability are identical either way, and the notes list every renamed property, which is the set you would need to re-annotate.

Why do @Serializable and the imports appear in some outputs but not others?

Because they are only needed when a name had to change. If every key in your sample is already a legal camelCase Kotlin property name, the output is plain data classes with no imports at all — paste them in and go. The moment one key needs a pin (a snake_case key, a key with a space, a keyword written in backticks), the annotation machinery becomes necessary for correctness, so @Serializable goes on every class in the file, @SerialName on the properties that need it, and both imports appear at the top. The decision is made for the whole file before any class is printed, so the set is consistent.

When does a property get a `= null` default?

Exactly when its type is nullable — which is when the sample shows the value can fail to be there: the key is missing from some elements of an array, or it is present but sometimes null. The default and the question mark arrive together, because in kotlinx.serialization a nullable property without a default still has to be present in the JSON (as an explicit null); the default is what lets an absent key deserialize. Required properties get neither.

Why is there a property written in backticks, like `class`?

Because your JSON has a key that is a Kotlin keyword. Kotlin lets you use a keyword as a name if you wrap it in backticks — val `class`: String is legal — and @SerialName above it pins the wire name the serializer actually looks for. The notes flag each one; if backticked names offend your style guide, rename the property and keep the @SerialName, which is doing the real work.

Why Long for integers?

Because the sample can only prove 'whole number'. Long is the width Kotlin's JSON tooling conventionally lands on, and it holds every integer a JSON document can realistically carry. If a field is truly an Int counter or a Short code, narrow it after generating — that is a fact about your domain the sample cannot know.

What do Any and Any? mean in the output?

Any is a slot whose type genuinely varies across the sample (a number in one element, a string in another) — there is no honest single type. Any? is a slot that is null everywhere it appears: nullable something, but the sample never shows the something. Both are listed in the notes with their reasons. Note that kotlinx.serialization cannot deserialize Any without a custom serializer — that is precisely the warning the notes are giving you: this slot needs a decision from you, not a guess from the generator.

Related tools