JSON 요소(JSON elements)
JSON 요소(JSON elements)
Kotlin 직렬화 라이브러리는 JSON을 구조적(structural) 수준에서 다루는 것도 지원해요. JsonElement API를 쓰면 JSON 구조를 Kotlin 타입이나 문자열로 변환하기 전에 직접 검사·수정·구축할 수 있습니다.
JsonElement에는 핵심 JSON 구조를 나타내는 세 가지 직접 하위 타입이 있어요.
JsonPrimitive는 문자열, 숫자, boolean,null같은 프리미티브 JSON 요소를 다뤄요.null값은JsonNull이라는JsonPrimitive의 특수 하위 클래스로 표현돼요. 각JsonPrimitive는 값의 문자열 표현을 저장하고, 이것을JsonPrimitive.content프로퍼티로 접근할 수 있어요.JsonArray는 JSON 배열을 나타내고,JsonElement항목들의 KotlinList예요.JsonObject는 JSON 객체를 나타내고,String키와JsonElement값을 가진 KotlinMap이에요.
출처: JSON elements
본문
문자열을 JSON 요소로 파싱하기
문자열을 JsonElement로 파싱하면, Kotlin 타입이나 문자열로 변환하기 전에 JSON 구조로 작업할 수 있어요.
Json.parseToJsonElement() 함수를 쓰면 입력을 디코딩·역직렬화하지 않고 JSON 요소 트리로 파싱할 수 있습니다.
// 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
JSON 요소의 내용에 접근하기
JsonElement API의 확장 프로퍼티로 JSON 요소의 내용에 직접 접근할 수 있어요. 이 확장 프로퍼티들은 요소를 특정 하위 타입으로 캐스팅하며, 요소가 예상한 JSON 구조가 아니면 IllegalArgumentException을 던져요.
사용할 수 있는 확장 프로퍼티는 다음과 같아요.
jsonPrimitive:JsonPrimitive를 반환해요.jsonArray:JsonArray를 반환해요.jsonObject:JsonObject를 반환해요.
마찬가지로 JsonPrimitive에도 값을 Kotlin 프리미티브 타입으로 파싱하는 확장 프로퍼티가 있어요. 예를 들어 int, intOrNull, long, longOrNull 같은 것들이 있죠.
다음은 구조를 알고 있는 JSON 데이터를 처리할 때 이 확장 프로퍼티들을 어떻게 쓰는지 보여 주는 예시예요.
// 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
JSON 구조를 미리 알 수 없다면 요소 타입을 확인하고 각 JsonElement 하위 타입을 명시적으로 처리하면 돼요. 예를 들어 when 표현식을 쓰는 헬퍼 함수를 만들 수 있죠.
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}"
}
JSON 요소 만들기
특정 JsonElement 하위 타입의 인스턴스를 직접 만들 수 있어요.
JsonPrimitive를 만들려면 JsonPrimitive() 함수를 사용하면 됩니다.
// 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
JsonArray와 JsonObject 요소는 직접 생성자를 호출하거나 빌더 함수를 사용해 만들 수 있어요.
List에서JsonArray를 만들려면JsonArray()를 쓰거나buildJsonArray()빌더 함수를 쓰면 돼요.Map에서JsonObject를 만들려면JsonObject()를 쓰거나buildJsonObject()빌더 함수를 쓰면 돼요.
빌더 함수는 Kotlin 표준 라이브러리의 컬렉션 빌더와 비슷한 DSL을 제공하며, JSON 전용 오버로드와 내부 빌더 함수가 포함돼 있어요.
다음은 JSON 빌더 DSL의 핵심 기능을 보여 주는 예시예요.
// 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
리터럴 JSON 내용 인코딩하기
JSON 명세는 숫자의 크기나 정밀도를 제한하지 않지만, JsonPrimitive() 함수로 임의 크기의 숫자를 직렬화하면 몇 가지 문제가 생길 수 있어요.
예를 들어 큰 숫자에 Double을 쓰면 값이 잘려 정밀도를 잃을 수 있어요. Kotlin/JVM의 BigDecimal을 쓰면 값은 정밀하게 유지되지만, JsonPrimitive()가 그 값을 숫자가 아니라 문자열로 인코딩합니다.
// 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
이 예시에서 pi는 소수점 30자리 숫자로 정의됐지만, 결과 JSON은 그 정밀도를 보존하지 못해요. Double 값은 소수점 15자리로 잘리고, String 값은 따옴표로 감싸져서 숫자가 아니라 JSON 문자열이 됩니다.
이런 문제를 피하려면 JsonUnquotedLiteral() 함수로 이 예시의 pi 문자열 값 같은 따옴표 없는 임의 값을 인코딩할 수 있어요.
// 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
pi를 다시 BigDecimal로 디코딩하려면 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
이 예시는 단순함을 위해 JsonPrimitive를 사용했어요. 더 재사용 가능한 방법은 JSON 변환하기를 참고해 주세요.
JSON null 리터럴
일관되지 않은 상태를 만들지 않도록, JsonUnquotedLiteral() 함수로 "null" 문자열을 인코딩할 수는 없어요. 시도하면 예외가 발생합니다.
// 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
JSON null 리터럴 값을 표현하려면 JsonNull을 사용하면 돼요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
//sampleStart
fun main() {
val possiblyNull = JsonNull
println(possiblyNull)
// null
}
//sampleEnd
Json 요소 디코딩하기
JsonElement 클래스의 인스턴스를 직렬화 가능한 객체로 디코딩하려면 Json.decodeFromJsonElement() 함수를 사용하면 됩니다.
// 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