skip to content

JsonElement & Dynamic JSON

JsonElement gives you a schemaless tree — objects, arrays, primitives — for JSON whose shape you do not know ahead of time, with a builder DSL for constructing it. Use it when a typed class genuinely cannot describe the payload.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

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

open as a page

Show how to construct ad-hoc JSON with the buildJsonObject {} and buildJsonArray {} DSLs. What builder functions are available inside the block?

level: middleimportance: must knowfreq 60%

basics

~10 s

buildJsonObject { } lets you assemble a JSON object by calling put("key", value) for primitives, and putJsonObject/putJsonArray for nested structures. buildJsonArray { } builds arrays with add(...). They return immutable JsonObject/JsonArray.

open as a page

Given an unknown JSON string, how do you parse it with parseToJsonElement and safely navigate optional/nested fields? Contrast the throwing accessors with the *OrNull variants.

level: middleimportance: should knowfreq 55%

basics

~20 s

Call Json.parseToJsonElement(text) to get a tree, then drill in with obj["key"]. For safety use the ...OrNull helpers (jsonObject vs the safe path, intOrNull, contentOrNull) plus Kotlin's ?. so a missing or wrong-typed field gives null instead of throwing.

open as a page

How do you convert between a typed @Serializable object and a JsonElement tree, and back? Explain encodeToJsonElement and decodeFromJsonElement and a use case for round-tripping through the tree.

level: seniorimportance: should knowfreq 45%

basics

~10 s

Json.encodeToJsonElement(value) turns a typed object into a JsonElement tree, and Json.decodeFromJsonElement<T>(element) turns a tree back into a typed object. Going through the tree lets you tweak, inspect, or merge fields between the two worlds.

open as a page

What are the immutability, equality, and ordering guarantees of JsonObject and JsonArray, and what gotchas arise when you treat JsonObject as a Map (e.g. number representation)?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

JsonObject and JsonArray are immutable. JsonObject acts like a Map and keeps insertion order; equality compares contents. A common gotcha: numbers are stored as text inside JsonPrimitive, so 1 and 1.0 are different primitives and aren't equal.

open as a page