내장 타입 직렬화하기

내장 타입 직렬화하기

Kotlin 직렬화 라이브러리는 다양한 내장 타입을 지원해요. 여기에는 프리미티브나 문자열 같은 기본 타입뿐 아니라 일부 표준 라이브러리 클래스도 포함돼요. 이어지는 절들에서 이런 타입들을 자세히 설명하고 직렬화하는 방법을 보여 드릴게요.

출처: Serialize built-in types

본문

기본 타입(Basic types)

Kotlin 직렬화는 직렬화 데이터에서 단일 값으로 표현되는 타입들을 위한 내장 직렬화기를 제공해요. 프리미티브, 문자열, enum이 여기에 해당합니다.

예를 들어 Long 타입을 직렬화하는 방법을 볼게요.

// Imports the necessary library declarations
import kotlinx.serialization.*
import kotlinx.serialization.json.*

//sampleStart
@Serializable
class Data(val signature: Long)

fun main() {
    val data = Data(0x1CAFE2FEED0BABE0)
    println(Json.encodeToString(data))
    // {"signature":2067120338512882656}
}
//sampleEnd

숫자(Numbers)

정수와 부동 소수점을 포함한 모든 Kotlin 숫자 타입을 자연스러운 JSON 표현으로 직렬화할 수 있어요.

import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlin.math.PI

//sampleStart
@Serializable
class Data(
    val answer: Int,
    val pi: Double
)                     

fun main() {
    val data = Data(42, PI)
    println(Json.encodeToString(data))
    // {"answer":42,"pi":3.141592653589793}
}
//sampleEnd

부호 없는 숫자(Unsigned numbers)

Kotlin 직렬화는 UByte, UInt 같은 Kotlin의 부호 없는 정수 타입을 지원해요. JSON에서는 이런 값들이 일반 JSON 숫자로 직렬화되며 부호 없는 전체 범위를 보존합니다.

import kotlinx.serialization.*
import kotlinx.serialization.json.*

//sampleStart
@Serializable
class Counter(val counted: UByte, val description: String)

fun main() {
    val counted = 239.toUByte()
    println(Json.encodeToString(Counter(counted, "tries")))
    // {"counted":239,"description":"tries"}
}
//sampleEnd

JSON이 부호 없는 숫자의 전체 범위를 보존하지만, 다른 직렬화 형식은 다르게 처리할 수 있어요. 예를 들어 ProtoBuf와 CBOR은 이 타입들을 부호 있는 대응 타입으로 직렬화합니다.

Long 숫자를 문자열로

Long 숫자를 JSON에서 문자열로 표현할 수 있어요. JavaScript 환경에서 유용한데, JavaScript의 Number 타입이 모든 Kotlin Long 값을 정밀하게 표현하지 못해 정밀도 손실이 생길 수 있기 때문이죠.

LongAsStringSerializer@Serializable 어노테이션과 함께 사용해 JSON에서 Long 값을 문자열로 인코딩할 수 있어요.

import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.json.*

//sampleStart
@Serializable
class Data(
    @Serializable(LongAsStringSerializer::class)
    val signature: Long
)

fun main() {
    val data = Data(0x1CAFE2FEED0BABE0)
    println(Json.encodeToString(data))
    // {"signature":"2067120338512882656"}
}
//sampleEnd

LongAsStringSerializer 같은 직렬화기를 파일의 모든 프로퍼티에 지정할 수도 있어요. 자세한 내용은 파일용 직렬화기 지정하기를 참고해 주세요.

Enum 클래스

모든 enum 클래스는 @Serializable 어노테이션 없이도 기본적으로 직렬화 가능해요. JSON으로 직렬화하면 enum은 문자열로 인코딩됩니다.

import kotlinx.serialization.*
import kotlinx.serialization.json.*

//sampleStart
// The @Serializable annotation isn't required for enum classes
enum class Status { SUPPORTED }

@Serializable
class Project(val name: String, val status: Status) 

fun main() {
    val data = Project("kotlinx.serialization", Status.SUPPORTED)
    println(Json.encodeToString(data))
    // {"name":"kotlinx.serialization","status":"SUPPORTED"}
}
//sampleEnd

Kotlin/JS나 Kotlin/Native를 대상으로 할 때는 encodeToString<Status>(Status.SUPPORTED)처럼 enum을 루트 객체로 사용하려면 enum 클래스에 @Serializable 어노테이션을 붙여야 해요.

enum 항목의 직렬 이름 커스터마이즈하기

enum 항목의 직렬 이름을 커스터마이즈하려면 @SerialName 어노테이션을 쓰고 enum 클래스를 @Serializable로 표시하면 됩니다.

import kotlinx.serialization.*
import kotlinx.serialization.json.*

//sampleStart
// Requires the @Serializable annotation because of @SerialName
@Serializable
enum class Status { @SerialName("maintained") SUPPORTED }

@Serializable
class Project(val name: String, val status: Status) 

fun main() {
    val data = Project("kotlinx.serialization", Status.SUPPORTED)
    println(Json.encodeToString(data))
    // {"name":"kotlinx.serialization","status":"maintained"}
}
//sampleEnd

직렬 이름 커스터마이즈에 대한 자세한 내용은 직렬 이름 커스터마이즈하기를 참고해 주세요.

표준 라이브러리 타입

Kotlin 직렬화는 표준 라이브러리의 여러 타입을 지원하지만, 범위(range)와 Regex 클래스 같은 일부 클래스는 지원하지 않아요.

Pair와 Triple

Kotlin 표준 라이브러리의 PairTriple 클래스를 직렬화할 수 있어요.

import kotlinx.serialization.*
import kotlinx.serialization.json.*

//sampleStart
@Serializable
class Project(val name: String)

fun main() {
    val pair = 1 to Project("kotlinx.serialization")
    println(Json.encodeToString(pair))
    // {"first":1,"second":{"name":"kotlinx.serialization"}}
}
//sampleEnd

컬렉션(Collections)

Kotlin 직렬화는 List, Set, Map의 읽기 전용과 변경 가능 변형 모두를 포함해 컬렉션 타입을 지원해요. ArrayListLinkedHashSet 같은 구체 구현, 그리고 제네릭·프리미티브 배열 타입도 지원합니다. 이 컬렉션들이 표현되는 방식은 직렬화 형식에 따라 달라져요.

JSON에서는 리스트와 셋이 JSON 배열로 직렬화되고, 맵은 JSON 객체로 표현됩니다.

Kotlin은 선언된 타입을 사용해 JSON을 역직렬화해요. 역직렬화 중 결과 객체의 타입은 소스 코드에 지정된 정적 타입으로 결정됩니다. 이 타입은 프로퍼티의 타입이거나 디코딩 함수의 타입 파라미터일 수 있어요.

리스트 직렬화하기

Kotlin 직렬화는 List 타입을 JSON 배열로 직렬화해요. 클래스 리스트의 예시를 볼게요.

import kotlinx.serialization.*
import kotlinx.serialization.json.*

//sampleStart
@Serializable
class Project(val name: String)

fun main() {
    val list = listOf(
        Project("kotlinx.serialization"),
        Project("kotlinx.coroutines")    
    )
    println(Json.encodeToString(list))
    // [{"name":"kotlinx.serialization"},{"name":"kotlinx.coroutines"}]
}
//sampleEnd

셋 직렬화하기

Set 타입은 List 타입과 마찬가지로 JSON 배열로 직렬화돼요.

import kotlinx.serialization.*
import kotlinx.serialization.json.*

//sampleStart
@Serializable
class Project(val name: String)

fun main() {
    val set = setOf(
        Project("kotlinx.serialization"),
        Project("kotlinx.coroutines")    
    )
    println(Json.encodeToString(set))
    // [{"name":"kotlinx.serialization"},{"name":"kotlinx.coroutines"}]
}
//sampleEnd

기본적으로 중복 항목이 있는 셋을 역직렬화할 수 있어요. 중복 처리 동작은 구현 정의(implementation-defined)입니다.

맵 직렬화하기

Kotlin 직렬화는 프리미티브나 enum 키를 가진 Map 타입을 지원해요.

import kotlinx.serialization.*
import kotlinx.serialization.json.*

//sampleStart
@Serializable
class Project(val name: String)

fun main() {
    // Creates a map with Int keys
    val map = mapOf(
        1 to Project("kotlinx.serialization"),
        2 to Project("kotlinx.coroutines")    
    )
    println(Json.encodeToString(map))
    // {"1":{"name":"kotlinx.serialization"},"2":{"name":"kotlinx.coroutines"}}
}
//sampleEnd

맵 직렬화는 형식에 따라 달라져요. JSON에서는 맵이 객체로 표현됩니다. JSON 객체 키는 항상 문자열이므로, Kotlin에서 숫자 키이더라도 키가 문자열로 인코딩돼요. CBOR 같은 다른 형식은 프리미티브가 아닌 키를 가진 맵을 지원하며 그대로 보존합니다.

JSON은 복잡하거나 합성된 키를 기본적으로 지원하지 않아요. 구조화된 객체를 맵 키로 인코딩하려면 구조화된 맵 키 허용하기를 참고해 주세요.

컬렉션의 역직렬화 동작

Kotlin은 선언된 타입을 사용해 JSON을 역직렬화해요. 예를 들어 컬렉션에서 List는 중복을 보존하지만 Set은 고유성을 강제합니다.

import kotlinx.serialization.*
import kotlinx.serialization.json.*

//sampleStart
@Serializable
data class Data(
    val a: List<Int>,
    val b: Set<Int>
)

fun main() {
    val data = Json.decodeFromString<Data>("""
        {
            "a": [42, 42],
            "b": [42, 42]
        }
    """)
    // Duplicates are removed from data.b because the Set type enforces unique elements
    println(data)
    // Data(a=[42, 42], b=[42])
}
//sampleEnd

Kotlin 컬렉션에 대한 자세한 내용은 컬렉션 개요를 참고해 주세요.

Unit과 싱글턴 객체

Kotlin의 Unit 타입과 다른 싱글턴 객체는 직렬화할 수 있어요. 싱글턴은 인스턴스가 하나뿐인 클래스로, 상태가 외부 프로퍼티가 아니라 객체 자체로 정의돼요. JSON에서 싱글턴 객체는 빈 구조로 직렬화됩니다.

import kotlinx.serialization.*
import kotlinx.serialization.json.*

//sampleStart
@Serializable
object SerializationVersion {
    val libraryVersion: String = "1.0.0"
}

fun main() {
    println(Json.encodeToString(SerializationVersion))
    // {}
    println(Json.encodeToString(Unit))
    // {}
}
//sampleEnd

직렬화된 싱글턴 객체는 추가 필드가 없는 경우를 나타내기 위해 닫힌 다형성 계층에서 사용할 수 있어요.

Duration과 Instant

Kotlin의 Duration 타입은 ISO-8601-2 형식을 사용해 문자열로 직렬화돼요.

import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlin.time.*

//sampleStart
fun main() {
    val duration = 1000.toDuration(DurationUnit.SECONDS)
    println(Json.encodeToString(duration))
    // "PT16M40S"
}
//sampleEnd

Kotlin의 Instant 타입도 ISO-8601-1 형식을 사용해 특정 시점을 나타내는 문자열로 직렬화할 수 있어요.

import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlin.time.*

//sampleStart
fun main() {
    val instant = Instant.fromEpochMilliseconds(1607505416124)
    println(Json.encodeToString(instant))
    // "2020-12-09T09:16:56.124Z"
}
//sampleEnd

Nothing

Nothing 타입은 기본적으로 직렬화 가능해요. 인스턴스가 없으므로 인코딩·디코딩하면 예외가 발생합니다. 제네릭 기본 타입을 가진 다형성 계층처럼 타입이 문법적으로 필요하지만 직렬화에는 관여하지 않을 때 Nothing을 사용하면 돼요.

import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.json.*

//sampleStart
@Serializable
sealed class ParametrizedParent<out R> {
    @Serializable
    data class ChildWithoutParameter(val value: Int) : ParametrizedParent<Nothing>()
}

fun main() {
    println(Json.encodeToString(ParametrizedParent.ChildWithoutParameter(42)))
    // {"value":42}
}
//sampleEnd

더 알아보기

  • 클래스 직렬화하기에서 클래스를 직렬화하고 @Serializable 어노테이션의 기본 동작을 수정하는 방법을 파고들 수 있어요.
  • 더 복잡한 JSON 직렬화 시나리오는 JSON 직렬화 개요를 참고해 주세요.
  • 공유 기본 클래스를 통한 다형성과 다양한 타입 직렬화에 대해 더 알고 싶다면 다형성 클래스 직렬화하기를 배워 볼 수 있어요.