Data classes for Android and Ktor
Kotlin projects decode JSON with typed data classes, and kotlinx.serialization is the multiplatform standard for it: it works on Android, the JVM, Kotlin/JS and Kotlin Native, and Ktor uses it out of the box. Writing the classes for a big response is repetitive. Paste a response body from your network inspector or API docs and get classes you can use immediately.
What gets generated
- Every JSON object becomes a
@Serializable data classwithvalproperties, and nested objects become their own classes named after their key. - JSON numbers become
Long(whole numbers) orDouble(anything with a decimal point or exponent); strings, booleans and arrays becomeString,BooleanandList<T>. - Keys that are not idiomatic Kotlin names are converted to camelCase, and
@SerialName("first-name")keeps the original key for decoding and encoding. - Objects inside one array are merged. A property missing from some items, or
nullin some, becomes nullable with a default:val nick: String? = null. Thanks to the default, decoding succeeds when the key is absent. - Values that are only ever
null, and arrays that mix types such as[1, "a"], are typed asJsonElement. kotlinx.serialization cannot decode a plain union, so you inspect the element at run time instead of risking a crash. - A JSON array at the root produces
typealias Root = List<RootElement>.
The file starts with the kotlinx imports and a usage comment: Json.decodeFromString<Root>(jsonString).
Root type name
The only option, Root type name (default Root), names the top-level class or type alias. It is converted to PascalCase, so api response becomes ApiResponse. Nested classes are named after their keys, and items of a plural key are singularised, so badges holds Badge objects.
Setup and tips
The classes need the serialization plugin and runtime: add kotlin("plugin.serialization") to your Gradle plugins and org.jetbrains.kotlinx:kotlinx-serialization-json to dependencies. Real APIs add fields over time, so create your decoder with Json { ignoreUnknownKeys = true } to avoid failures when they do.
Inference is based only on your sample. Values written exactly as a calendar date (2024-03-11) become java.time.LocalDate, and the file then includes a small LocalDateSerializer registered with @file:UseSerializers — a JVM and Android type, so replace it with kotlinx.datetime.LocalDate in multiplatform code. Full timestamps stay String, and repeated strings are not turned into enums. Integers beyond 2^53 are flagged, since the sample cannot be read exactly; Long covers up to 2^63. For Jackson- or Gson-based projects, the Java class generator produces annotated POJOs instead. Generation runs locally in this tab, and the Kotlin formatter can restyle the result.
Examples
Profile response for an Android app
snake_case keys become camelCase properties with @SerialName, and expires_at becomes a nullable JsonElement with a null default.
{
"user_id": 42,
"display_name": "Ben Okafor",
"avatar_url": "https://cdn.example.com/u/42.png",
"followers": 1280,
"settings": { "dark_mode": true, "language": "en" },
"badges": [
{ "code": "early", "awarded_at": "2024-03-11" },
{ "code": "mod", "awarded_at": "2025-01-20", "expires_at": null }
]
}// Decode with kotlinx.serialization:
// val data = Json.decodeFromString<Profile>(jsonString)
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
@Serializable
data class Profile (
@SerialName("user_id")
val userID: Long,
@SerialName("display_name")
val displayName: String,
@SerialName("avatar_url")
val avatarURL: String,
val followers: Long,
val settings: Settings,
val badges: List<Badge>
)
@Serializable
data class Badge (
val code: String,
@SerialName("awarded_at")
val awardedAt: String,
@SerialName("expires_at")
val expiresAt: JsonElement? = null
)
@Serializable
data class Settings (
@SerialName("dark_mode")
val darkMode: Boolean,
val language: String
)
Array root with optional fields
The root becomes a List type alias, and due is a nullable LocalDate with a null default because only one item has it.
[
{ "id": 1, "title": "Write release notes", "done": true },
{ "id": 2, "title": "Review pricing", "done": false, "due": "2026-10-01" }
]// Decode with kotlinx.serialization:
// val data = Json.decodeFromString<Todos>(jsonString)
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
typealias Todos = List<Todo>
@Serializable
data class Todo (
val id: Long,
val title: String,
val done: Boolean,
val due: String? = null
)
Common errors and how to fix them
| Error | Cause | Fix |
|---|---|---|
Object keys must be in double quotes: found …Explained | The sample is a Kotlin or JavaScript map literal rather than JSON. | Quote every key with double quotes. |
Unexpected '<' where a value was expectedExplained | An HTML error page was pasted instead of the JSON response. | Copy the actual response body from the network inspector. |
Unexpected end of inputExplained | The response was truncated when copying, for example from a log line. | Copy the complete JSON document. |
Frequently asked questions
Does it support Moshi or Gson?
The annotations target kotlinx.serialization. For Moshi or Gson, replace @SerialName with @Json(name=…) or @SerializedName and drop @Serializable.
Why is a property typed JsonElement?
Its value was always null in the sample, or it mixed types. JsonElement decodes safely whatever arrives, and you can narrow it once you know the real type.
Why do nullable properties have = null?
The default lets kotlinx.serialization decode objects where the key is missing entirely, which is what made the property optional in the first place.
Is my JSON uploaded?
No. Inference and code generation run in your browser.