JSON to Kotlin Data Class
Generate Kotlin data classes with kotlinx.serialization or Jackson annotations from sample JSON — entirely on this device.
The generated Kotlin appears here as you type.
Press ? for shortcuts
Do it offline:$ npx quicktype sample.json --lang kotlin --framework kotlinx --acronym-style camel --top-level Root --no-maps --no-enums --no-uuids --no-integer-strings --no-boolean-strings -o Models.kt
How to use it
- Paste a payload or drop a .json file; the Kotlin pane redraws after you stop typing.
- Open a tab per response variant — a key one variant omits comes back nullable with a default of null.
- Pick kotlinx.serialization, Jackson or plain classes, then set the top-level name, package and acronym casing.
- Copy the file, download the .kt file, or share a permalink carrying every sample and setting.
Paste a payload — or several — and read Kotlin back: one data class per object shape, @SerialName carrying any key an identifier cannot spell, and a nullable type only where a sample proves the key can go missing. Every JSON integer arrives as Long, because no sample can promise a value stays inside Int. The generator runs in this tab.
Examples
{"order_id": 4021, "customer email": "sam@example.com", "shipped_at": null}import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
@Serializable
data class Root (
@SerialName("order_id")
val orderId: Long,
@SerialName("customer email")
val customerEmail: String,
@SerialName("shipped_at")
val shippedAt: JsonElement? = null
)
All three keys need renaming to reach a Kotlin property, and each keeps its wire spelling in @SerialName — including the one holding a space. shipped_at was null everywhere, so it lands as JsonElement? defaulted to null.
{"cart id": 88, "coupon_code": "SPRING"}data class Root (
val cartId: Long,
val couponCode: String
)
Plain classes drop the imports, the @Serializable line and any record of what the wire said: nothing maps couponCode back to coupon_code. A chip names every key that lost its spelling.
{"device_id": "kit-9", "firmware": "1.4.2"}
{"device_id": "kit-3"}import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
@Serializable
data class Device (
@SerialName("device_id")
val deviceId: String,
val firmware: String? = null
)
The second reading has no firmware, so it is declared String? = null while deviceId, present in both, stays non-null. firmware shows the quiet half of the rule too: a name already matching its key needs no annotation.
{"amount": 1.0, "count": 3, "ratio": 0.5}import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
@Serializable
data class Root (
val amount: Long,
val count: Long,
val ratio: Double
)
1.0 has no fractional part, so it is read as an integer and amount is typed Long — a chip says so. Only ratio, carrying a real fraction, becomes Double.
[1, 2, 3]typealias Root = ArrayList<Long>
A top level that is not an object has no properties to declare, so the file states the type it really is rather than refusing.
{"id": 7, "customer": {"first_name": "Ada", "vip": true}, "tags": ["a", "b"]}import com.fasterxml.jackson.annotation.*
data class Order (
@get:JsonProperty(required=true)@field:JsonProperty(required=true)
val id: Long,
@get:JsonProperty(required=true)@field:JsonProperty(required=true)
val customer: Customer,
@get:JsonProperty(required=true)@field:JsonProperty(required=true)
val tags: List<String>
)
data class Customer (
@get:JsonProperty("first_name", required=true)@field:JsonProperty("first_name", required=true)
val firstName: String,
@get:JsonProperty(required=true)@field:JsonProperty(required=true)
val vip: Boolean
)
A nested object earns its own declaration in the same file, named after the key. Jackson marks a property required when every sample carried it, and annotates getter and field alike.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
A question mark is evidence, not politeness
Nothing is optional until a sample omits it, so a single payload produces a file with no nullable property at all. Open the error shape in its own tab and the question marks settle where they belong.
Always null means no type was ever seen
A key that is null in every sample gets JsonElement? under kotlinx.serialization and Any? under plain classes, both defaulted to null, with a chip naming it. The generator declines to invent a type.
Every integer is Long, and past Long it is Double
A sample proves a value was seen once, never that it stays inside Int, so integers are declared Long. A literal too big even for Long widens to Double and loses digits — carry that one as a String.
Keys Kotlin cannot spell
object and fun are hard keywords and become rootObject and rootFun; 123start opens with a digit and becomes the123Start; 名前 is a legal identifier already and is left alone. Where user_id, userId and user id share one object they would collapse, so the generator forces them apart as purpleUserId, rootUserId and userId — trust the annotations, not the names.
The package line is yours to add
There is no package by default, which keeps the file droppable into any source root. Type one and it heads the file; type something Kotlin would reject and the legalized spelling is emitted with a chip naming it.
Sixteen nested arrays is the ceiling
An array directly inside an array costs the Kotlin generator roughly double per level, so a seventeenth is refused rather than left to hang the tab, whichever framework is on. Nesting through objects is cheap and runs to the documented 256-level cap.
Common errors
Sample 2: Unexpected '}' at line 1, column 9 — probable cause: trailing comma after the last property.- Nothing is merged until every open tab parses. The prefix names the failing tab and shows only with several open; a caret frame marks the character.
Input is larger than the 10 MB limit for this tool.- Past the combined ceiling the run is refused whole instead of trimmed behind your back. Shape comes from variety rather than volume: keep a few records here and point the command below at the export file.
Samples total 2.0 MB — above the 2 MB advisory band. Generate from a representative sample, not the whole export.- An advisory, not a refusal: generation waits for the run shortcut instead of firing on every keystroke, because a payload this size costs real time.
FAQ
Why did a key my API always sends come back as String?
Because one of the open tabs does not carry it. Optionality is read from the samples, never guessed from the name, so a truncated paste flips a property. Close that tab and the question mark goes with it.
What does data class give me over a plain class here?
equals, hashCode, toString, copy and componentN over the constructor properties — so two decoded payloads with equal fields compare equal, and copy(status = "shipped") amends one without a builder. An empty JSON object declares no properties, so it renders as a plain class.
Is the generated Kotlin checked by a compiler before I copy it?
No — there is no kotlinc in the environment that builds this page. Every example is replayed through the generator in continuous integration and checked byte for byte, with identifier and bracket-balance oracles standing in. Build the file in your own module before trusting it.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Run the generator now, whatever the sample size.
- ⌘/Ctrl + S
- Write the .kt file to disk instead of saving the page.
- ⌘/Ctrl + C
- Copies the Kotlin pane when no text is selected.
- ⌘/Ctrl + Shift + C
- Copy the whole file even mid-selection.
- t
- Switch theme, unless the caret sits in a field.
- ? or ⌘/Ctrl + /
- Show the shortcut overlay.
- Esc
- Close whatever is on top.