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.
answer
- encodeToJsonElement: typed -> tree
- decodeFromJsonElement<T>: tree -> typed
- Skips the encodeToString/decodeFromString round-trip
- Honours the same Json config (encodeDefaults, explicitNulls)
- Foundation of JsonTransformingSerializer / polymorphic-by-content
basics
~10 sJson.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.
solid answer
~40 sThe `Json` instance bridges typed and dynamic JSON. `Json.encodeToJsonElement(value)` (with a `@Serializable` type) produces a `JsonElement` tree instead of a string; `Json.decodeFromJsonElement<T>(element)` reads a tree into a typed `T`. This avoids an intermediate string round-trip via `encodeToString`/`decodeFromString`. Typical uses: serialise a domain object, then inject or strip dynamic fields with `buildJsonObject { ... }` before sending; or accept an unknown payload, patch it in the tree, then `decodeFromJsonElement` into your model. It's also how you implement custom serializers that operate structurally (`JsonTransformingSerializer` / `JsonContentPolymorphicSerializer` build on this). Both functions respect the `Json` instance's config (e.g. `encodeDefaults`, `explicitNulls`). Errors surface as `SerializationException` when the tree doesn't match the target type.
code
kotlin · 12 linesimport kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable data class Event(val type: String, val at: Long)
val json = Json
val tree = json.encodeToJsonElement(Event("login", 1700L))
val withSource = buildJsonObject {
tree.jsonObject.forEach { (k, v) -> put(k, v) }
put("source", "mobile")
}
val roundTripped: Event = json.decodeFromJsonElement(withSource) // ignores unknown? depends on configgo deeper
Knows there are functions to turn an object into a tree and back, without details.
Uses encodeToJsonElement/decodeFromJsonElement correctly and avoids the string round-trip.
Explains config inheritance, failure modes, and a real augment/patch use case with buildJsonObject.
Connects it to custom/transforming/polymorphic serializers and decides where typed<->dynamic boundaries belong in a system.
## The bridge functions A configured `Json` instance offers element-level conversions that skip the string step: - **`Json.encodeToJsonElement(value)`** — serialises a `@Serializable` value into a `JsonElement` (typically a `JsonObject`). Equivalent in spirit to `encodeToString`, but stays in the tree domain. - **`Json.decodeFromJsonElement<T>(element)`** — deserialises a `JsonElement` into a typed `T`. Equivalent to `decodeFromString` but takes a tree. These let you move between the **typed world** (`data class`) and the **dynamic world** (`JsonElement`) without `toString()` parsing in between. ```kotlin import kotlinx.serialization.* import kotlinx.serialization.json.* @Serializable data class User(val id: Int, val name: String) val json = Json val tree: JsonElement = json.encodeToJsonElement(User(1, "Ada")) val back: User = json.decodeFromJsonElement(tree) ``` ## Why round-trip through the tree 1. **Augment or strip fields.** Encode a domain object, then add audit/metadata fields the class doesn't model: ```kotlin val enriched = buildJsonObject { json.encodeToJsonElement(User(1, "Ada")).jsonObject.forEach { (k, v) -> put(k, v) } put("serverTime", System.currentTimeMillis()) } ``` 2. **Patch unknown input then type it.** Receive a `JsonElement`, normalise/rename keys in the tree, then `decodeFromJsonElement<MyModel>` once it matches. 3. **Structural custom serializers.** `JsonTransformingSerializer<T>` overrides `transformSerialize`/`transformDeserialize` on `JsonElement`s; `JsonContentPolymorphicSerializer` inspects the tree to choose a concrete type. Both rely on this element-level path. ## Config is honoured Both functions use the **same `Json` configuration** as string encoding — `encodeDefaults`, `explicitNulls`, `namingStrategy`, etc. So `encodeToJsonElement` of a class with a default value omits it unless `encodeDefaults = true`, exactly as the string path would. ## Failure modes `decodeFromJsonElement` throws `SerializationException` (e.g. `MissingFieldException`) if the tree lacks a required field or has the wrong type — same contract as decoding from a string. ## When NOT to bridge If you never need the dynamic shape, encode/decode strings directly; the tree adds an allocation. Bridge only when you must inspect or reshape the JSON between the wire and your model.
- Does decodeFromJsonElement ignore unknown keys?Only if the Json instance has ignoreUnknownKeys = true; otherwise an unknown key throws, same as string decoding.
- How is this related to JsonTransformingSerializer?That serializer hooks transformSerialize/transformDeserialize at the JsonElement level, using exactly this typed<->tree bridge to reshape JSON during (de)serialization.
saying these in an interview costs you the question
- Claiming encodeToJsonElement ignores Json config like encodeDefaults
- Going through tree.toString() then decodeFromString instead of decodeFromElement (extra parse)
- Assuming decodeFromJsonElement can't throw on a malformed/incomplete tree
- Not knowing it underpins JsonTransformingSerializer / content-polymorphic serializers