skip to content

In kotlinx.serialization, what is JsonElement and what are its main subtypes? When would you use it instead of a typed @Serializable data class?

level: juniorimportance: must knowfreq 70%

answer

  1. Sealed root: JsonObject / JsonArray / JsonPrimitive (+ JsonNull)
  2. JsonObject is a Map, JsonArray is a List
  3. parseToJsonElement gives you the tree
  4. Use it when schema is dynamic/unknown
  5. .jsonObject / .jsonPrimitive accessors are throwing casts

basics

~20 s

JsonElement is a flexible in-memory tree for JSON when you don't have a fixed class. Its parts are JsonObject (key-value), JsonArray (list), and JsonPrimitive (string, number, boolean, or null). Use it when the JSON shape is unknown or changes.

solid answer

~40 s

JsonElement is the sealed root type of kotlinx.serialization's schemaless JSON tree. Its subtypes are JsonObject (a Map<String, JsonElement>), JsonArray (a List<JsonElement>), and JsonPrimitive (string/number/boolean), with JsonNull as a special primitive. You reach for it instead of a typed @Serializable data class when the schema is dynamic, partially unknown, or varies per record, when you only need a few fields out of a large blob, or when you must transform/merge raw JSON without modelling it. You obtain a tree via Json.parseToJsonElement(string) or by serializing a value with Json.encodeToJsonElement. A typed class is preferred when the schema is stable because it gives compile-time safety and is faster; JsonElement trades that safety for flexibility.

code

kotlin · 6 lines
kotlin
import kotlinx.serialization.json.*

val tree = Json.parseToJsonElement("""{"items":[1,2,3],"ok":true}""")
val obj = tree.jsonObject
val count = obj["items"]!!.jsonArray.size      // 3
val ok = obj["ok"]!!.jsonPrimitive.boolean     // true

go deeper

for a junior

Names the three subtypes and knows it's a flexible tree for unknown JSON.

for a middle

Knows JsonObject is a Map / JsonArray is a List, can read primitives, and justifies dynamic-vs-typed trade-offs.

for a senior

Discusses performance/compile-time-safety trade-offs and the throwing-cast accessors and JsonNull-vs-absent distinction.

for a principal

Frames when to expose a dynamic boundary (untyped ingest) vs typed domain, and migration to typed classes as schema stabilises.

## What JsonElement is `JsonElement` is an abstract `sealed` class in `kotlinx.serialization` (package `kotlinx.serialization.json`) that represents JSON as an in-memory tree, **without** binding it to any Kotlin class. "Schemaless" means you do not declare the structure ahead of time. ## The subtypes - **`JsonObject`** — a JSON object `{ ... }`. It implements `Map<String, JsonElement>`, so you can index it with `obj["key"]` and iterate entries. - **`JsonArray`** — a JSON array `[ ... ]`. It implements `List<JsonElement>`, so `arr[0]` and `for (e in arr)` work. - **`JsonPrimitive`** — a leaf value: a string, number, or boolean. A string primitive has `isString == true`; numbers/booleans are stored as their textual `content`. - **`JsonNull`** — the JSON `null`. It is itself a `JsonPrimitive` (with `isString == false`). Because `JsonElement` is `sealed`, a `when` over it can be exhaustive. ## How you get one ```kotlin import kotlinx.serialization.json.* val json = Json val tree: JsonElement = json.parseToJsonElement("""{"name":"Ada","age":36}""") val obj: JsonObject = tree.jsonObject // cast helper, throws if not an object val name: String = obj["name"]!!.jsonPrimitive.content // "Ada" val age: Int = obj["age"]!!.jsonPrimitive.int // 36 ``` The `.jsonObject`, `.jsonArray`, and `.jsonPrimitive` **accessor properties** are casts that throw `IllegalArgumentException` if the element is the wrong kind. On a `JsonPrimitive` you read typed values via `content` (raw string), `int`, `long`, `double`, `boolean`, or the nullable `intOrNull`, `booleanOrNull`, etc. ## When to prefer JsonElement over a typed class Use `JsonElement` when: - The schema is **unknown or dynamic** (e.g. a generic webhook payload). - The shape **varies per record** or evolves frequently. - You only need to **peek at or transform** a few fields and don't want to model the whole blob. - You need to **merge, filter, or re-emit** raw JSON. Prefer an `@Serializable` `data class` when the schema is **stable**: you get compile-time field checks, cleaner code, and better performance, because the typed path avoids walking a generic tree.

  • Is JsonNull the same as a Kotlin null?
    No. JsonNull is a JsonElement representing the literal JSON null; a missing key returns Kotlin null (absent) from obj["key"]. They are distinguishable.
  • How do you read a number from a JsonPrimitive?
    Use the int/long/double extension properties (or *OrNull variants). content gives the raw string form.

A typed data class is a labelled form; JsonElement is a blank notebook you can read or scribble in any shape.

saying these in an interview costs you the question

  • Thinking JsonElement is part of org.json or Jackson rather than kotlinx.serialization
  • Claiming JsonObject is a List or JsonArray is a Map
  • Saying you must always use a data class and JsonElement is never needed
  • Confusing JsonNull (a literal null element) with an absent key

context