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)?
answer
- JsonObject/JsonArray are immutable read-only views
- JsonObject = LinkedHashMap → insertion-ordered, Map equality
- Structural equals/hashCode (value semantics)
- Primitives store numbers as text: 1 != 1.0
- isString separates "5" from 5
basics
~20 sJsonObject 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.
solid answer
~40 s`JsonObject` and `JsonArray` are **immutable** read-only views — `JsonObject` delegates to a `LinkedHashMap` (insertion-ordered) and `JsonArray` to a `List`. Both implement structural `equals`/`hashCode`, so two trees with the same contents are equal. Because `JsonObject` *is* a `Map<String, JsonElement>`, you can use `keys`, `entries`, `getValue`, `forEach`, etc. The big gotcha is that a `JsonPrimitive` stores numbers as their **raw string content**, with no inherent numeric type: `JsonPrimitive(1)` and `JsonPrimitive(1.0)` have different `content` ("1" vs "1.0") and are **not** equal, and there's no normalisation. Likewise a string `JsonPrimitive` carries `isString = true` to distinguish `"5"` from `5`. So equality/hashing is textual at the leaf level. Mutation requires rebuilding via `buildJsonObject`/`buildJsonArray`.
code
kotlin · 7 linesimport kotlinx.serialization.json.*
val a = buildJsonObject { put("x", 1) }
val b = buildJsonObject { put("x", 1) }
println(a == b) // true (structural)
println(JsonPrimitive(1) == JsonPrimitive(1.0)) // false ("1" vs "1.0")
println(JsonPrimitive(1).double == JsonPrimitive(1.0).double) // true (parsed)go deeper
Knows the trees are read-only and act like Map/List.
Knows insertion order is preserved and trees compare structurally.
Explains the textual number storage, 1 vs 1.0 inequality, isString, and rebuilding for edits.
Reasons about canonical/ordered output for signing/diffing and how textual storage avoids numeric precision loss in pipelines.
## Immutability `JsonObject` and `JsonArray` are **immutable**. `JsonObject` wraps a `Map<String, JsonElement>` (backed by a `LinkedHashMap`) and exposes it read-only; `JsonArray` wraps a `List<JsonElement>`. There are no setters and no `put`/`add` on the finished objects — to change one you create a new tree with the `buildJsonObject {}` / `buildJsonArray {}` DSL. ## Ordering Because the backing map is a `LinkedHashMap`, `JsonObject` preserves **insertion order** of keys, and serialising it back out keeps that order. `JsonArray` obviously preserves element order. ## Equality and hashCode Both types implement **structural** `equals`/`hashCode` inherited from their `Map`/`List` contracts: two `JsonObject`s are equal iff they hold the same key→value pairs (order does **not** affect Map equality), and two `JsonArray`s are equal iff same elements in same order. This makes them safe as test-assertion targets and map keys. ## The number-representation gotcha A `JsonPrimitive` does **not** store a typed number — it stores the **raw textual `content`** plus an `isString` flag. Consequences: ```kotlin import kotlinx.serialization.json.* JsonPrimitive(1) == JsonPrimitive(1.0) // false: content "1" vs "1.0" JsonPrimitive("5") == JsonPrimitive(5) // false: isString differs ("5" string vs 5 number) JsonPrimitive(1).content // "1" JsonPrimitive(1).int // 1 (parsed on demand) ``` So equality is **lexical** at the leaf. `1` and `1.0` are distinct elements; the library does not normalise numeric forms. When you need numeric comparison, read both via `.double`/`.int` and compare the parsed values, not the primitives. Large/precise numbers survive because they're kept as text (no premature `Double` rounding) until you ask for a specific type. ## String vs number ambiguity `isString` lets the tree distinguish the JSON token `"5"` (string) from `5` (number). Use `JsonPrimitive(value: String)` for strings and the numeric/boolean overloads otherwise; `JsonUnquotedLiteral` exists for emitting raw unquoted content (e.g. big decimals) but must be used carefully. ## Practical implications - Treat trees as values: compare, hash, and store them freely, but never expect to mutate in place. - Don't rely on `JsonPrimitive` equality for numeric logic — compare parsed values. - Insertion order is stable, which matters for canonical output or signing.
- Why are JsonPrimitive(1) and JsonPrimitive(1.0) not equal?A primitive stores the raw textual content ("1" vs "1.0") and equality is lexical; there's no numeric normalisation.
- How do you 'edit' a JsonObject?You can't mutate it; rebuild a new one with buildJsonObject, copying the entries you keep and putting the new ones.
saying these in an interview costs you the question
- Claiming JsonObject is mutable / has put on the finished object
- Asserting JsonPrimitive(1) equals JsonPrimitive(1.0)
- Saying key order is lost (it's a LinkedHashMap, order preserved)
- Ignoring isString so "5" and 5 are treated as equal