The Kotlin serialization library also supports working with JSON at a structural level. You can use the JsonElement API, to inspect, modify, and build JSON structures directly before converting them into a Kotlin type or a string.
JsonElement has three direct subtypes that represent the core JSON structures:
JsonPrimitive handles primitive JSON elements such as strings, numbers, booleans, and null. The null value is represented by a special subclass of JsonPrimitive, called JsonNull. Each JsonPrimitive stores a string representation of its value, which you can access through its JsonPrimitive.content property.
JsonArray represents a JSON array. It's a Kotlin List of JsonElement items.
JsonObject represents a JSON object. It's a Kotlin Map with String keys and JsonElement values.
Parse strings to JSON elements
You can parse a string into a JsonElement to work with the JSON structure before converting it into a Kotlin type or a string.
Use the Json.parseToJsonElement() function to parse the input into a JSON element tree without decoding or deserializing it:
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
//sampleStart
fun main() {
val element = Json.parseToJsonElement("""
{"name":"kotlinx.serialization","language":"Kotlin"}
""")
// JsonElement.toString() gives you a valid JSON string
println(element)
// {"name":"kotlinx.serialization","language":"Kotlin"}
}
//sampleEnd
Access the contents of JSON elements
You can access the contents of a JSON element directly through the extension properties of the JsonElement API. These extension properties cast the element to a specific subtype and throw an IllegalArgumentException if the element doesn't have the expected JSON structure.
Here's an example of how you can use these extension properties when processing JSON data with a known structure:
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
//sampleStart
fun main() {
val element = Json.parseToJsonElement("""
{
"name": "kotlinx.serialization",
"forks": [{"votes": 42}, {"votes": 9000}, {}]
}
""")
val sum = element
// Accesses the forks key from the JsonObject
.jsonObject["forks"]!!
// Accesses the value as a JsonArray and sums the votes values from each JsonObject as Int
.jsonArray.sumOf { it.jsonObject["votes"]?.jsonPrimitive?.int ?: 0 }
println(sum)
// 9042
}
//sampleEnd
If you don't know the JSON structure in advance, you can check the element type and handle each JsonElement subtype explicitly. For example, you can use a helper function with a when expression:
fun processElement(element: JsonElement): String = when (element) {
is JsonObject -> "JsonObject with keys: ${element.keys}"
is JsonArray -> "JsonArray with ${element.size} elements"
is JsonPrimitive -> "JsonPrimitive with content: ${element.content}"
}
Create JSON elements
You can create instances of specific JsonElement subtypes directly.
To create a JsonPrimitive, use the JsonPrimitive() function:
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
//sampleStart
fun main() {
// Creates JsonPrimitive values from different Kotlin primitives
val number = JsonPrimitive(42)
val text = JsonPrimitive("kotlinx.serialization")
println(number)
// 42
println(text)
// "kotlinx.serialization"
}
//sampleEnd
You can create JsonArray and JsonObject elements either by directly calling their constructors or by using the builder functions:
Here's an example that highlights the key features of the JSON builder DSLs:
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
//sampleStart
fun main() {
val element = buildJsonObject {
// Adds a simple key-value pair to the JsonObject
put("name", "kotlinx.serialization")
// Adds a nested JsonObject under the owner key
putJsonObject("owner") {
put("name", "kotlin")
}
// Adds a JsonArray with multiple JsonObjects
putJsonArray("forks") {
// Adds a JsonObject to the JsonArray
addJsonObject {
put("votes", 42)
}
addJsonObject {
put("votes", 9000)
}
}
}
// Prints the resulting JSON string
println(element)
// {"name":"kotlinx.serialization","owner":{"name":"kotlin"},"forks":[{"votes":42},{"votes":9000}]}
}
//sampleEnd
Encode literal JSON content
While the JSON specification doesn't restrict the size or precision of numbers, serializing numbers of arbitrary size with the JsonPrimitive() function might lead to some issues.
For example, if you use Double for large numbers, the value might get truncated and you lose precision. If you use Kotlin/JVM BigDecimal, the value stays precise, but JsonPrimitive() encodes the value as a string rather than as a number:
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import java.math.BigDecimal
//sampleStart
val format = Json { prettyPrint = true }
fun main() {
val pi = BigDecimal("3.141592653589793238462643383279")
// Converts the BigDecimal to a Double, causing potential truncation
val piJsonDouble = JsonPrimitive(pi.toDouble())
// Converts the BigDecimal to a String, preserving the precision but treating it as a string in JSON
val piJsonString = JsonPrimitive(pi.toString())
val piObject = buildJsonObject {
put("pi_double", piJsonDouble)
put("pi_string", piJsonString)
}
println(format.encodeToString(piObject))
// "pi_double": 3.141592653589793,
// "pi_string": "3.141592653589793238462643383279"
}
//sampleEnd
In this example, even though pi is defined as a number with 30 decimal places, the resulting JSON doesn't preserve that precision. The Double value is truncated to 15 decimal places, and the String value is wrapped in quotes, so it becomes a JSON string rather than a number.
To avoid these issues, you can encode an arbitrary unquoted value, such as the string value of pi in this example, using the JsonUnquotedLiteral() function:
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import java.math.BigDecimal
//sampleStart
val format = Json { prettyPrint = true }
fun main() {
val pi = BigDecimal("3.141592653589793238462643383279")
// Encodes the raw JSON content using JsonUnquotedLiteral()
@OptIn(ExperimentalSerializationApi::class)
val piJsonLiteral = JsonUnquotedLiteral(pi.toString())
// Converts to Double and String
val piJsonDouble = JsonPrimitive(pi.toDouble())
val piJsonString = JsonPrimitive(pi.toString())
val piObject = buildJsonObject {
put("pi_literal", piJsonLiteral)
put("pi_double", piJsonDouble)
put("pi_string", piJsonString)
}
// pi_literal now accurately matches the value defined
println(format.encodeToString(piObject))
// "pi_literal": 3.141592653589793238462643383279,
// "pi_double": 3.141592653589793,
// "pi_string": "3.141592653589793238462643383279"
}
//sampleEnd
To decode pi back to a BigDecimal, extract the string content of the JsonPrimitive:
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import java.math.BigDecimal
//sampleStart
fun main() {
val piObjectJson = """
{
"pi_literal": 3.141592653589793238462643383279
}
""".trimIndent()
// Decodes the JSON string into a JsonObject
val piObject: JsonObject = Json.decodeFromString(piObjectJson)
// Extracts the string content from the JsonPrimitive
val piJsonLiteral = piObject["pi_literal"]!!.jsonPrimitive.content
// Converts the string to a BigDecimal
val pi = BigDecimal(piJsonLiteral)
// Prints the decoded value of pi, preserving all 30 decimal places
println(pi)
// 3.141592653589793238462643383279
}
//sampleEnd
JSON null literal
To avoid creating an inconsistent state, you can't encode the string "null" with the JsonUnquotedLiteral() function. If you try to do so, an exception is thrown:
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
//sampleStart
@OptIn(ExperimentalSerializationApi::class)
fun main() {
JsonUnquotedLiteral("null")
// Exception in thread "main" kotlinx.serialization.json.internal.JsonEncodingException
}
//sampleEnd
To represent a JSON null literal value, use JsonNull:
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
//sampleStart
fun main() {
val possiblyNull = JsonNull
println(possiblyNull)
// null
}
//sampleEnd
Decode Json elements
To decode an instance of the JsonElement class into a serializable object, use the Json.decodeFromJsonElement() function:
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
//sampleStart
@Serializable
data class Project(val name: String, val language: String)
fun main() {
val element = buildJsonObject {
put("name", "kotlinx.serialization")
put("language", "Kotlin")
}
// Decodes the JsonElement into a Project object
val data = Json.decodeFromJsonElement<Project>(element)
println(data)
// Project(name=kotlinx.serialization, language=Kotlin)
}
//sampleEnd