DevKitHub

Programming

JSON to Kotlin Data Class Converter

Paste a JSON response to get Kotlin data classes with a @SerialName, @Json or @SerializedName annotation on every property. Every element of an array is merged into one class, so properties that are sometimes null or missing come out nullable.

14 lines
31 lines

Decode

Root type
Root

The classes describe the sample you paste: paste an array of several responses so that properties which are sometimes missing get a null default. kotlinx.serialization needs its compiler plugin, and Moshi its KSP or kapt code generator.

This tool runs entirely in your browser. Your input is never uploaded, stored or logged.

How it works

Every sample you paste is merged position by position before any Kotlin is written, and the types promise only what the samples showed. A key with a value in every sample is non-null, such as val login: String. One that is null in some sample is nullable with no default, String?, and one missing from some objects is nullable with a default, String? = null. The default is the part that matters. kotlinx.serialization treats a property as optional only when it has a default value; without one, a missing key fails with "Field 'license' is required for type with serial name 'Repo', but it was missing", even though the type allows null. The default also changes what is written back, because kotlinx.serialization leaves out a property whose value equals its default: a key that was missing stays missing, while a nullable property with no default is written as null. Unknown keys are refused with "Encountered an unknown key" until the Json instance is built with ignoreUnknownKeys = true.

Integers are Long and other numbers Double. kotlinx.serialization will not read 1.0 into a Long, failing with "Unexpected symbol '.' in numeric literal", so a property is Long only when every sample is written without a fraction or an exponent. A value neither holds exactly, such as 18446744073709551615, is a JsonPrimitive with kotlinx.serialization, whose content keeps the digits as written; BigInteger or BigDecimal with Gson; and Double, with a warning, with Moshi, which has no adapter for either. kotlinx.serialization has no serializer for Any, so a property whose type differs between samples is a JsonElement there, and Any? with Moshi or Gson, which fill it with Double for every number. Dates stay String, which every library reads without extra setup.

Property names are lowerCamelCase from the key, and the key itself goes in the annotation, so user_id, user-id and userId all give userId. A hard keyword such as class, fun or val keeps its name in backticks, `class`, while soft keywords such as value or data are ordinary names. Keys that give the same name are numbered, and a class never takes a name the code refers to, such as Serializable or JsonElement, which it would shadow. With Moshi, @JsonClass(generateAdapter = true) needs Moshi’s code generator applied through KSP or kapt, or Moshi fails with "Failed to find the generated JsonAdapter class". Gson is the trap. It knows nothing about Kotlin and creates objects without calling their constructor, so it ignores non-null types and, unless every property has one, default values: a val name: String can hold null after decoding, and the NullPointerException comes later, wherever name is first used.

Common problems

Every example below is run against this tool in our test suite, so what it says here is what the tool actually does.

Unexpected character "U" where a JSON value was expected.

User(id=1, name=Ada, email=null)
Why:
A data class printed with println or toString(), which Kotlin writes as User(id=1, name=Ada). It is debugging text, not JSON: strings are unquoted and null is indistinguishable from the text null.
Fix:
Print Json.encodeToString(user), or log the raw response body, and paste that.

Trailing comma: JSON does not allow a comma after the last item of an array.

{"tags": ["a", "b",]}
Why:
A trailing comma, which Kotlin allows in argument and parameter lists and which kotlinx.serialization accepts only when the Json instance sets allowTrailingComma = true.
Fix:
Remove the comma after the last element before pasting.

Field 'email' is required for type with serial name 'User', but it was missing at path: $

Why:
Every sample had the key, so the property has no default, and a real response leaves it out. kotlinx.serialization fails on a missing key without a default even when the type is nullable.
Fix:
Paste a sample without the key, or add = null by hand. Json { explicitNulls = false } also reads a missing nullable property as null.

Encountered an unknown key 'x' at path: $

Why:
The response has a key that none of the samples had. kotlinx.serialization refuses unknown keys by default; Moshi and Gson skip them.
Fix:
Build the Json instance with ignoreUnknownKeys = true, or annotate the class with @JsonIgnoreUnknownKeys.

Serializer for class 'Root' is not found. Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.

Why:
The classes are annotated @Serializable, but the kotlinx.serialization compiler plugin is not applied to the module, so no serializer was generated for them.
Fix:
Add the plugin org.jetbrains.kotlin.plugin.serialization, at the same version as the Kotlin plugin, next to the kotlinx-serialization-json dependency.

Frequently asked questions

Should I use kotlinx.serialization, Moshi or Gson?
kotlinx.serialization is JetBrains’ own library, generates its serializers at compile time with a compiler plugin, and works on every Kotlin platform, including Multiplatform. Moshi is common on Android with Retrofit and also respects Kotlin nullability and defaults. Gson predates Kotlin and ignores both, so it is best kept for existing code.
How do I decode a JSON array into a list?
With kotlinx.serialization, Json.decodeFromString<List<RootItem>>(text). With Moshi, moshi.adapter<List<RootItem>>(Types.newParameterizedType(List::class.java, RootItem::class.java)). With Gson, gson.fromJson<List<RootItem>>(text, object : TypeToken<List<RootItem>>() {}.type).
Why val and not var?
A decoded response is usually read, not changed, and val makes the data class immutable; copy(name = "x") gives a changed copy. Choose var for a model that a form edits in place. All three libraries read both.
Why are dates String rather than Instant?
Because each library needs a date serializer of its own: kotlinx-datetime’s types for kotlinx.serialization, an adapter for Moshi, a TypeAdapter for Gson. Once one is set up, change the type of the property by hand.

Last updated