JSON Errors / Kotlin

JsonDecodingException: Unexpected JSON token at offset

How to Fix: JsonDecodingException: Unexpected JSON token at offset

kotlinx.serialization stopped decoding because the JSON did not match what your @Serializable class expects. The message tells you where and why: "Unexpected JSON token at offset 0: Expected start of the object '{', but had '[' instead at path: $". The offset is the 0-based character position, and the path ($, $.user.name, $[3]) is where in the structure decoding failed.

Read the reason after the colon. Most cases are a mismatch between the JSON and your Kotlin types, not broken JSON. The exception often ends with a hint naming the Json {} setting that would accept the input, plus a "JSON input:" excerpt around the failure.

{"find": "error"}paste your JSON below

Common causes

1. Expected start of the object '{', but had '[' instead

The response is an array but you decoded a single object (or the reverse). Decode into List<User> instead of User, or unwrap the envelope object first.

Invalid
Json.decodeFromString<User>(body)        // body is [{...}, {...}]
Valid
Json.decodeFromString<List<User>>(body)

2. Expected string literal but 'null' literal was found

The API sent null for a property you declared as non-null. Make the property nullable (val nickname: String?), or give it a default value and enable coerceInputValues so null falls back to the default.

3. But had '<' instead: the server returned HTML

An error page, a redirect to a login page, or a wrong base URL in Retrofit or Ktor. Check the HTTP status and log the raw body before decoding.

4. Encountered an unknown key 'x'

The JSON has a field your class does not declare. By default kotlinx.serialization rejects it so you notice API changes. Older versions report this as "Unexpected JSON token at offset N: Encountered an unknown key"; current versions use "Encountered an unknown key 'x' at offset N".

How to fix it

Configure a tolerant Json instance once
val json = Json {
    ignoreUnknownKeys = true   // new API fields don't break old clients
    coerceInputValues = true   // null -> property default for non-null fields
    explicitNulls = false      // missing keys -> null for nullable fields
}

val users = json.decodeFromString<List<User>>(body)
Model the JSON honestly
@Serializable
data class User(
    val id: Long,
    val name: String,
    val nickname: String? = null,          // API may send null or omit it
    @SerialName("created_at") val createdAt: String,
)
Use the same Json in Retrofit and Ktor
// Retrofit: com.squareup.retrofit2:converter-kotlinx-serialization (2.11+)
Retrofit.Builder()
    .baseUrl(BASE_URL)
    .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
    .build()

// Ktor client
HttpClient {
    install(ContentNegotiation) { json(json) }
}

Frequently asked questions

Should I just turn on isLenient?

Only for input you know is non-standard, such as unquoted keys or strings from a legacy system. isLenient does not fix type mismatches, nulls, or unknown keys - those have their own settings (coerceInputValues, ignoreUnknownKeys). For comments and trailing commas, newer versions have allowComments and allowTrailingComma.

How do I find the problem in a large response?

Use the path in the message ($.items[12].price) rather than counting characters to the offset. To inspect the raw document, log the body before decoding, or paste it into a JSON viewer and navigate to that path.

What is MissingFieldException?

A related error: a non-null property without a default was absent from the JSON entirely ("Field 'x' is required ... but it was missing"). Give the property a default value or make it nullable with = null.

Last updated:

Related JSON errors

You might also need

Free and in-browser, like everything on JSON Console. Browse all tools →