본문으로 바로가기
반응형

이 글은 android 기준으로 설명을 진행합니다.

하기 링크를 참고 하였습니다.

github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md

또한 로그 출력을 위해서 println이나 이와 동일한 역할을 하는 LogInfo를 사용 합니다. (LogInfo는 안드로이드에서 로그출력을 위해 제가 따로 만들어 놓은 Top-Level function 입니다.)

이 글은 여러개의 series로 구성되었습니다.

2020/10/22 - [개발이야기/Kotlin] - [Kotlinx serialization] Json 직렬화/역직렬화 - Fast apply #1

2020/11/03 - [개발이야기/Kotlin] - [Kotlinx serialization] 기본 사용법#2

2020/11/17 - [개발이야기/Kotlin] - [Kotlinx serialization] Json 직렬화/역직렬화 - Builtin classes #3

2020/12/04 - [개발이야기/Kotlin] - [Kotlinx serialization] Json 직렬화/역직렬화 -Polymorphism #5

2020/12/15 - [개발이야기/Kotlin] - [Kotlinx serialization] Json 직렬화/역직렬화 -JSON features #6




출처: https://tourspace.tistory.com/357?category=797357 [투덜이의 리얼 블로그]

앞선 포스팅에서 object를 Json format으로 변경하기 위해 @Serializable annotation을 사용했습니다.

이 annotation을 사용하면 자동으로 파생된 serializer가 객체의 각 요소를 특정 JSON 같은 format으로 변경합니다.

예를 들어 Color라는 클래스에 @Serializable을 사용하면 하래와 같이 처리 됩니다.

@Serializable
class Color(val rgb: Int)

fun main() {
    val green = Color(0x00ff00)
    println(Json.encodeToString(green))
}

{"rgb":65280}

인자로 넣은 rgb값이 수자로 변환되어 json에 표시 됩니다.

Plugin-generated serializer

위에서 사용한 Color 클래스처럼 @Serializable을 붙인 class에서는 KSerializer interfcace를 구현한 (Kotlin serialization compiler plugin에서 자동으로 생성한) instance를 얻을 수 있습니다.

얻는 방법은 해당 class의 companion object에 선언된 serializer()를 호출하면 됩니다.

일단 이 serializer를 이용하면 class의 serialization에 대한 구조를 설명하는 descriptor property를 확인할 수 있습니다.

fun main() {
    val colorSerializer: KSerializer<Color> = Color.serializer()
    println(colorSerializer.descriptor)
} 

Color(rgb: kotlin.Int)

이렇게 자동 생성되어 반환되는 serializer는 Kotlin serialization framework에서 일반적인 class로 정의되었을 때나, 다른 class의 param으로 들어갔을때 serialize를 하기위해 사용됩니다.

단! serializable한 class의 companion object 내부에 serializer()를 임의로 정의 할 수는 없습니다.

 

Plugin-gernerated generic serializer

Generic class의 경우 자동으로 생성되는 serializer()는 해당 class의 param의 개수만큼의 type parameter를 허용합니다.

이 parameter의 type은 KSerializer로 실제 사용할 인자에 대한 serializer가 제공 되어야 합니다.

말로는 이해하기 어려우나 아래 예제로는 쉽게 이해 가능합니다.

@Serializable           
@SerialName("Box")
class Box<T>(val contents: T)    

fun main() {
    val boxedColorSerializer = Box.serializer(Color.serializer())
    println(boxedColorSerializer.descriptor)
} 

Box는 generic 값을 param으로 받습니다.

따라서 serializer()를 호출할 때 인자 값으로 실제 사용할 class(type)의 serializer()의 넣어줘야 합니다.

Box(contents: Color)

Builtin primitive serializers

기본 type인 경우 extention function으로 정의된 serializer를 호출 할 수있습니다.

fun main() {
    val intSerializer: KSerializer<Int> = Int.serializer()
    println(intSerializer.descriptor)
}

PrimitiveDescriptor(kotlin.Int)

Constructing collection serializers

Builtin 으로 제공되는 collection serializer를 이용하기 위해서는 명시적으로 어떤 collection을 사용할지 표기해야 합니다. 예를 들어 ListSerializer(), SetSerializer(), MapSerializer()등을 말합니다.

또한 이런 클래스들은 generic을 사용하기 때문에 인스턴스로 만들기 위해서는 정해진 입력 parameter의 개수만큼 사용되는 실제 serializer를 표기해야 합니다.

따라서 List<String>의 serializer를 얻기 위한 예제는 다음과 같습니다.

  val stringListSerializer: KSerializer<List<String>> = ListSerializer(String.serializer())
  LogError(TAG) {stringListSerializer.descriptor.toString()}

kotlin.collections.ArrayList(PrimitiveDescriptor(kotlin.String))

Using top-level serializer function

만약 코드상에 임의의 Kotlin type이 있다면 top level fuction으로 정의된 serializer<T>()를 이용하여 serializer를 반환하도록 할 수 있습니다.

private fun test() {
    val stringToColorMapSerializer: KSerializer<Map<String, Dept>> = serializer()
    LogError(TAG) {stringToColorMapSerializer.descriptor.toString()}
}

@Serializable
class Dept(val no: Int, val name:String, val location:String)

kotlin.collections.LinkedHashMap(PrimitiveDescriptor(kotlin.String), Dept(no: kotlin.Int, name: kotlin.String, location: kotlin.String))

serializer<T>() 함수는 Serializers.kt에 정의된 top-level function 입니다.

//Serializers.kt

/**
 * Retrieves a serializer for the given type [T].
 * This method is a reified version of `serializer(KType)`.
 */
public inline fun <reified T> serializer(): KSerializer<T> {
    return serializer(typeOf<T>()).cast()
}

 

Custom serialzers

계속해서 serializer를 언급한 이유는 custom 하게 serializer를 만들어 원하는 형태로 출력되도록 할 수 있기 때문입니다.

이제 맨 처음 사용했던 Color 클래스를 custom하게 serialize 하는 방법을 알아봅니다.  

 

Primitive serializer

맨 처음 예제에서 hex 값으로 녹색을 나타내는 0x00ff00 값을 넣었을때 json으로 변경시 십진수로 변환되어 출력됩니다.

하지만 hex값을 그대로 json에 표현하고 싶다면 아래와 같이 Color class를 위해 KSerializer interface를 상속받은 custom serializer object를 생성합니다.

public interface KSerializer<T> : SerializationStrategy<T>, DeserializationStrategy<T>

KSerializerSerializationStrategyDeserializationStrategy 두개의 interface를 상속받습니다.

object ColorAsStringSerializer : KSerializer<Color> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Color", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: Color) {
        val string = value.rgb.toString(16).padStart(6, '0')
        encoder.encodeString(string)
    }

    override fun deserialize(decoder: Decoder): Color {
        val string = decoder.decodeString()
        return Color(string.toInt(16))
    }
}

Custom serializer는 세가지 요소로 구성 됩니다.

  • SerializationStrategy interface를 구현하기 위해 serialize 함수를 override 합니다. 이 함수는 Encode instance와 serialize할 값을 param으로 전달 받습니다. 함수 내부에서 이 값을 어떻게 표현할지 정하여 encoder의 encodeXXX()함수로 넘겨 줍니다. 이때 값을 어떤 primitive type으로 표현할지에 따라서 encodeXXX() 함수가 정해 집니다.
//KSerializer.kt

public interface SerializationStrategy<in T> {
/**
 * Describes the structure of the serializable representation of [T], produced
 * by this serializer.
 */
public val descriptor: SerialDescriptor

/**
 * Serializes the [value] of type [T] using the format that is represented by the given [encoder].
 * [serialize] method is format-agnostic and operates with a high-level structured [Encoder] API.
 * Throws [SerializationException] if value cannot be serialized.
 *
 * Example of serialize method:
 * ```
 * class MyData(int: Int, stringList: List<String>, alwaysZero: Long)
 *
 * fun serialize(encoder: Encoder, value: MyData): Unit = encoder.encodeStructure(descriptor) {
 *     // encodeStructure encodes beginning and end of the structure
 *     // encode 'int' property as Int
 *     encodeIntElement(descriptor, index = 0, value.int)
 *     // encode 'stringList' property as List<String>
 *     encodeSerializableElement(descriptor, index = 1, serializer<List<String>>, value.stringList)
 *     // don't encode 'alwaysZero' property because we decided to do so
 * } // end of the structure
 * ```
 */
public fun serialize(encoder: Encoder, value: T)

Encoding.kt 파일 내부에 각 primitive type에 따른 encode 함수를 지원하는걸 확인할 수 있습니다.

//Encoding.kt

...
public fun encodeInt(value: Int)

/**
 * Encodes a 64-bit integer value.
 * Corresponding kind is [PrimitiveKind.LONG].
 */
public fun encodeLong(value: Long)

/**
 * Encodes a 32-bit IEEE 754 floating point value.
 * Corresponding kind is [PrimitiveKind.FLOAT].
 */
public fun encodeFloat(value: Float)

/**
 * Encodes a 64-bit IEEE 754 floating point value.
 * Corresponding kind is [PrimitiveKind.DOUBLE].
 */
public fun encodeDouble(value: Double)

/**
 * Encodes a string value.
 * Corresponding kind is [PrimitiveKind.STRING].
 */
public fun encodeString(value: String)

/**
 * Encodes a enum value that is stored at the [index] in [enumDescriptor] elements collection.
 * Corresponding kind is [SerialKind.ENUM].
 *
 * E.g. for the enum `enum class Letters { A, B, C, D }` and
 * serializable value "C", [encodeEnum] method should be called with `2` as am index.
 *
 * This method does not imply any restrictions on the output format,
 * the format is free to store the enum by its name, index, ordinal or any other
 */
public fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int)
...

 

  • DeserializationStrategy interface를 구현하기 위해 deserialize함수를 override 합니다. param으로 넘어온 Decoder를 이용하여 값을 반환 받습니다. 이때 값의 형태에 따라 encode 함수에서 사용했던 primitive type을 반환하는 decodeXXX() 함수를 이용합니다. 그리고 반환되어야 할 객체를 생성하여 return 해 줍니다.
  • descriptor 속성은 format implementation이 어떤 encoding/decoding method를 호출하는지 미리 알수 있도록 정확하게 표현되어야 합니다. 일부 format은 serialized data의 구조를 생성하는데 사용될수도 있습니다. 또한 PrimitiveSerialDescriptor 함수는 각 primitive type에 따라 고유하게 정의된 PrimitiveKind 를 이용하여 정의 되어야 합니다.

이 세가지 속성을 만족하는 object를 선언했으니 실제 Color class에 적용해 봅니다.

적용할때는 with property를 이용합니다.

@Serializable(with = ColorAsStringSerializer::class)
class Color(val rgb: Int)

fun main() {
    //serialize
    val green = Color(0x00ff00)
    println(Json.encodeToString(green))
    
    //deserialize
    val color = Json.decodeFromString<Color>("\"00ff00\"")
    println(color.rgb)
}  

"00ff00"

65280

두개의 값이 출력됩니다.

또한 이 class는 다른 class에서도 사용이 가능합니다.

@Serializable(with = ColorAsStringSerializer::class)
data class Color(val rgb: Int)

@Serializable 
data class Settings(val background: Color, val foreground: Color)

fun main() {
    val data = Settings(Color(0xffffff), Color(0))
    val string = Json.encodeToString(data)
    println(string)
    require(Json.decodeFromString<Settings>(string) == data)
}  

{"background":"ffffff","foreground":"000000"}

Composite serializer via surrogate

이번에는 Color의 rgb 값을 각각 분리하는 custom serializer를 만들어 보겠습니다. encoding을 하면 r,g,b값을 각각 분리해서 표현하고, 이를 decoding하면 다시 Color객체로 생성되도록 합니다.

다만 직접 customize한 serializer를 작성하기 보단 이렇게 동작하는 다른 class를 이용하도록 합니다.

이를 대리 클래스(surrogate class)라고 합니다.

먼저 r,g,b값을 각각 나눠서 가진 ColorSurrogate class를 생성합니다.

@Serializable
@SerialName("Color")
private class ColorSurrogate(val r: Int, val g: Int, val b: Int) {
    init {     
        require(r in 0..255 && g in 0..255 && b in 0..255)
    }
}

일단 기본 builtin generator를 이용하기 위해 @Serializable을 붙였습니다.

그리고 color처럼 보이기 위해서 SerailName을 "Color"로 명명합니다.

다른 Serialize class와 같이 init()에 제약 조건을 넣을 수 있으며, private class로 사용할 수 있습니다.

이렇게 구성하며, 기본 생성된 ColorSurrogate의 serializer를  ColorSurrogate.serializer()를 통해서 얻을수 있습니다.

object ColorSerializer : KSerializer<Color> {
    override val descriptor: SerialDescriptor = ColorSurrogate.serializer().descriptor

    override fun serialize(encoder: Encoder, value: Color) {
        val surrogate = ColorSurrogate((value.rgb shr 16) and 0xff, (value.rgb shr 8) and 0xff, value.rgb and 0xff)
        encoder.encodeSerializableValue(ColorSurrogate.serializer(), surrogate)
    }

    override fun deserialize(decoder: Decoder): Color {
        val surrogate = decoder.decodeSerializableValue(ColorSurrogate.serializer())
        return Color((surrogate.r shl 16) or (surrogate.g shl 8) or surrogate.b)
    }
}

이제 ColorSerializer를 재구성 합니다.

custom하게 만들지만 내부적으로는 ColorSurrogate 클래스의 serialzier를 이용하므로 descriptor 역시 ColorSurrogate.serializer().descriptor를 이용하여 그대로 할당해 줍니다.

serialize 함수에서는 수신받은 Color객체의 value(rbg값)을 shift and masking 하여 R, G, B 값으로 각각 분리하여 ColorSurrogate객체를 생성합니다.

encoder는 encodeSerializableValue() 함수를 이용하며, 인자로 surrogate 객체와 이를 serialize할 serializer를 넘겨 줍니다.

deserialize역시 decodeSerailizableValue()에 surrogate 객체의 serializer를 넘겨주고 surrogate 객체를 복원합니다.

그리고 surrogate 객체의 r,g,b 값으 조합하여 Color 객체를 생성하여 반환합니다

결론적으로 code상에서는 Color객체를 사용하지만 실제 json 표현 방식은 SurrogateColor 객체의 형태로 수행됩니다.

@Serializable(with = ColorSerializer::class)
class Color(val rgb: Int)

이제 color class를 serialize하면 아래와 같이 표현 됩니다.

{"r":0,"g":255,"b":0}

물론 이 json을 다시 decoding하면 Color 객체가 반환됩니다.

Hand-written composite serializer

추가적인 할당으로 인한 성능 문제를 피하기 위해서나 동적으로 구성되는 properties에 대한 serializeation이 필요한 경우 Surrogate class를 만들어 사용할수 없습니다.

다른말로 surrogate class를 이용할 수 없는 경우 자동적으로 생성되는 serializer 처럼 직접 한땀한땀 만들어야 합니다.

object ColorAsObjectSerializer : KSerializer<Color> {
   ...
}

 

이제 해당 클래스에 들어갈 내용을 하나씩 채워 봅니다.

- Descriptor

override val descriptor: SerialDescriptor =
    buildClassSerialDescriptor("Color") {
        element<Int>("r")
        element<Int>("g")
        element<Int>("b")
    }

descriptor는 BuildClassSerialDescriptor builder를 이용해서 정의됩니다. 

Builder DSL 안에 있는 element function은 정해진 타입에 대응하는 serializer로 fetch 됩니다.

index는 0부터 시작하며, 순서대로 들어가야 합니다.

- serialize

serialize 함수는 encodeStructure DSL을 이용합니다.

override fun serialize(encoder: Encoder, value: Color) =
	encoder.encodeStructure(descriptor) {
	    encodeIntElement(descriptor, 0, (value.rgb shr 16) and 0xff)
	    encodeIntElement(descriptor, 1, (value.rgb shr 8) and 0xff)
	    encodeIntElement(descriptor, 2, value.rgb and 0xff)
	}

encodeStructure의 block안에서는 CompositeEncoder에 접근할 수 있습니다.

포스팅 첫 부분에서 사용했던 Encoder와 여기서 사용한 CompositeEncoder는 사용하는 함수가 encodeXXX와 encodeXXXElement라는 함수 이름의 차이 정도만 있습니다. 여기서도 순서는 descriptor와 동일하게 맞춰야 합니다.

- deserialize

마지막으로 가장 복잡한 deserialize 입니다.

override fun deserialize(decoder: Decoder): Color =
    decoder.decodeStructure(descriptor) {
        var r = -1
        var g = -1
        var b = -1
        while (true) {
            when (val index = decodeElementIndex(descriptor)) {
                0 -> r = decodeIntElement(descriptor, 0)
                1 -> g = decodeIntElement(descriptor, 1)
                2 -> b = decodeIntElement(descriptor, 2)
                CompositeDecoder.DECODE_DONE -> break
                else -> error("Unexpected index: $index")
            }
        }
        require(r in 0..255 && g in 0..255 && b in 0..255)
        Color((r shl 16) or (g shl 8) or b)
    }

decoder의 decodeStructure를 호출하여 CompositeDecoder에 접근합니다.

CompositeDecoder 블럭{..} 안에서는 반복문을 돌며 index를 하나씩 뽑고 decodeIntElement를 통해서 index에 들어가 있는 값을 뽑아 냅니다.

이때 index에 들어있는 값 type에 따라서 decodeXXXElement를 사용하며, 예제에서는 Int 값이므로 decodeIntElement를 사용했습니다.

이렇게 만들어진 예제를 아래와 같이 사용하면 위에서 surrogate class를 사용한 것과 동일한 값을 얻어낼 수 있습니다.

@Serializable(with = ColorAsObjectSerializer::class)
data class Color(val rgb: Int)

fun main() {
    val color = Color(0x00ff00)
    val string = Json.encodeToString(color) 
    println(string)
    require(Json.decodeFromString<Color>(string) == color)
}

{"r":0,"g":255,"b":0}

Sequential decoding protocol (experimental)

위에서 사용한 deserialize function은 어떤 format이든 decoding 가능하나, JSON 처럼 순서대로 저장하는 경우 decodeElementIndex를 호출하여 반복문을 사용하는것 보다는, CompositeDecode.decodeSequentially 를 사용하여 좀더 빠르게 뽑아낼 수 있습니다.

override fun deserialize(decoder: Decoder): Color =
    decoder.decodeStructure(descriptor) {
        var r = -1
        var g = -1
        var b = -1     
        if (decodeSequentially()) { // sequential decoding protocol
            r = decodeIntElement(descriptor, 0)           
            g = decodeIntElement(descriptor, 1)  
            b = decodeIntElement(descriptor, 2)
        } else while (true) {
            when (val index = decodeElementIndex(descriptor)) {
                0 -> r = decodeIntElement(descriptor, 0)
                1 -> g = decodeIntElement(descriptor, 1)
                2 -> b = decodeIntElement(descriptor, 2)
                CompositeDecoder.DECODE_DONE -> break
                else -> error("Unexpected index: $index")
            }
        }
        require(r in 0..255 && g in 0..255 && b in 0..255)
        Color((r shl 16) or (g shl 8) or b)
    }

decodeSequentially()를 호출하여 true가 반환되면 그냥 순서대로 값을 뽑아 내도록 합니다.

실제 plugin-generated로 생성되는 serializer도 위 코드와 유사하게 작성됩니다.

Serializing 3rd party classes

간혹 java.util.Date 같이  외부에서 제공되는 data type에 대한 serialize가 필요한 경우가 있습니다.

이는 위에서 했던 방법과 유사하게 KSerializer를 상속 받아 사용하되, time을 ms로 표현 하도록 합니다.

object DateAsLongSerializer : KSerializer<Date> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG)
    override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time)
    override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong())
}

지금까지 사용한 형태로 이 Serializer를 사용하기 위해서는 Date 클래스에 @Serializable를 붙이고 with property에 DateAsLongSerializer를 지정해 줘야 합니다.

하지만 Date class는 java.util에 있는 class로 우리가 수정할수 없는 코드이므로 다른 형태로 사용하는 방법을 사용하도록 합니다.

Passing a serializer manually

encodeToXXX 와 decodeFromXXX 함수들은 첫번째 인자로 serializer를 받을수 있는 overload된 함수들이 존재합니다.

따라서 위와 같이 @Serializable이 아닌 클래스들은 아래와 같이 serializer를 할당해 줄 수 있습니다.

fun makeJson() {
    val date = Date()
    LogInfo(TAG) { date.time.toString() }
    LogInfo(TAG) { Json.encodeToString(DateAsLongSerializer, date) }
}

1606382033309

출력값이 정상적으로 출력되었음을 알수 있습니다.

JSON 형태로 출력되지 않는 이유는 descriptor를 PrimitiveSerialDescriptor로 사용하기 때문입니다.

JSON 형태로 출력하기 이미 언급된 custom serializer를 DateAsLongSerializer2를 만들어 보겠습니다.

object DateAsLongSerializer2 : KSerializer<Date> {
        override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Date") {
            element<Long>("stamp")
        }

        override fun serialize(encoder: Encoder, value: Date) {
            encoder.encodeStructure(descriptor) {
                encodeLongElement(descriptor, 0, value.time)
            }
        }

        override fun deserialize(decoder: Decoder) =
            decoder.decodeStructure(descriptor) {
                var time = 0L
                while (true) {
                    when (val index = decodeElementIndex(descriptor)) {
                        0 -> time = decodeLongElement(descriptor, 0)
                        CompositeDecoder.DECODE_DONE -> break
                        else -> error("Unexpected index: $index")
                    }
                }
                Date(time)
            }
    }

{"stamp":1606382352574}

Specifying serializer on a property

만약 Date처럼 non-serializable한 class가 Serializable 한 클래스의 속성으로 존재한다면 아래 예제처럼 with를 이용합니다.

@Serializable          
class ProgrammingLanguage(
    val name: String,
    @Serializable(with = DateAsLongSerializer::class)
    val stableReleaseDate: Date
)

fun main() {
    val data = ProgrammingLanguage("Kotlin", SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00"))
    println(Json.encodeToString(data))
}

{"name":"Kotlin","stableReleaseDate":1606382352574}

물론 위에서 추가로 생성한 DateAsLongSerializer2를 사용해도 결과는 동일합니다.

 

Specifying serializers for a file

전체 코드에서 해당 클래스에 대한 serializer를 지정 하려면 소스코드 파일 최 상단에 아래와 같이 annotation을 추가합니다.

@file:UseSerializers(DateAsLongSerializer::class)

이 처럼 file level에서 선언해 놓으면 사용시에는 아래와 같이 with 없이 사용이 가능합니다.

@Serializable          
class ProgrammingLanguage(val name: String, val stableReleaseDate: Date)

fun main() {
    val data = ProgrammingLanguage("Kotlin", SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00"))
    println(Json.encodeToString(data))
}

{"name":"Kotlin","stableReleaseDate":1606382352574}

Custom serializers for a generic type

포스팅 상단에서 언급되었던 Box<T> 클래스를 재 활용해 보겠습니다.

위에서는 @Serializable을 사용하여 자동 생성되는 serializer를 이용했지만 이번엔 custom하게 만든 BoxSerializer를 이용해 보도록 하겠습니다.

@Serializable(with = BoxSerializer::class)
data class Box<T>(val contents: T)

class BoxSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Box<T>> {
    override val descriptor: SerialDescriptor = dataSerializer.descriptor
    override fun serialize(encoder: Encoder, value: Box<T>) = dataSerializer.serialize(encoder, value.contents)
    override fun deserialize(decoder: Decoder) = Box(dataSerializer.deserialize(decoder))
}

 위에서 기본적으로 custom serializer를 만들때 object를 사용했었습니다.

하지만 Generic을 사용하는 경우 반드시 class로 정의해야 하며, generic을 사용하는 param의 개수만큼 KSerializer를 인자로 갖는 생성자를 만들어야 합니다.

위 예제에서는 속성이 contents 하나 이므로 BoxSerializer로 하나의 KSerializer param을 같는 생성자를 갖도록 합니다.

@Serializable
data class Project(val name: String)

fun main() {
    val box = Box(Project("kotlinx.serialization"))
    val string = Json.encodeToString(box)
    println(string)
    println(Json.decodeFromString<Box<Project>>(string))
}

실제로 사용을 위해서 Project라는 Serializable한 class를 하나 만들었습니다.

그리고 Box의 contents 변수에 Project를 하나 생성해서 넣어줍니다.

따라서 Box<Project> 형태가 되겠죠?

{"name":"kotlinx.serialization"}
Box(contents=Project(name=kotlinx.serialization))

실제 표현된 json을 보면 Project를 직접 serialize한것처럼 보입니다.

Format-specific serializers

지금까지 언급된 custom serializer는 어떤 포멧이든 동일한 형태로 동작합니다.

하지만 특정 format의 기능을 갖도록 serializer를 만들수도 있습니다.

추후 포스팅할 Json 관련 부분에서 JSON의 특정 기능을 사용하는 serializer에 대해 얘기합니다.

또한 format 구현을 설명 하는 부분에서 format에 따라 타입을 표현하는 방법에 대한 내용을 원문의 Alternative and custom formats chapter에서 언급하고 있으나, 이 chapter는 experimental이라 제가 추후 포스팅할지는 좀더 고민해 보겠습니다.

Contextual serialization

지금까지 언급했던 custom serialization 전략들은 전부 컴파일 타임에 결정되는 정적인 형태 입니다.

(물론 Passing a serializer manually 섹션에서 encode / decode시에 serialzer를 직접 지정해서 넣을수는 있었습니다.)

이번에는 runtime에 동적으로 serializer를 선택 가능하도록 하는 contextual serialization에 대해서 설명합니다.

이는 ContextualSerializer에 의해서 지원 가능하며, 두가지 형태로 정의 가능합니다.

  • with를 이용하여 직접 annotation으로 정의 -> @Serializable(with = ContextualSerializer::class)
  • file-level에서 annotation 정의 (UseSerializers에서 했던것 처럼) -> UseContextualSerialization

위에서 사용했었던 Date class를 동적으로 처리해 보겠습니다.

@Serializable          
class ProgrammingLanguage(
    val name: String,
    @Contextual 
    val stableReleaseDate: Date
)

serializer를 동적으로 사용할 부분에 @Contexual annotation을 붙여줍니다.

이제 context에 상응하는 serialize 함수를 반드시 제공해야 encodeToXXX / decodeFromXXX를 할때 exception이 발생하는걸 막을 수 있습니다.

Serialzers module

runtime에 어떤 serializer를 사용할지를 정의하기 위해서 SerializersModule instance를 사용합니다.

이 모듈을 만들기 위해서 SerializersModuleBuilder DSL을 아래와 같이 용합니다.

private val module = SerializersModule { 
    contextual(DateAsLongSerializer)
}

그리고 이 모듈을 사용하기 위해 Json을 만들때 serializersModule property를 이용합니다.

val format = Json { serializersModule = module }

만약 다른 Serializer를 동적으로 사용해야 한다면 module을 추가로 만들고 Json을 생성하는 이 부분에서 분기하면 됩니다.

val format = if (isRepresentAsTime) {
        Json {serializerModule = module}
    } esle {
        Json {serializerModule = xxx}
    }

전체 코드는 아래와 같습니다.

object DateAsLongSerializer : KSerializer<Date> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG)
    override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time)
    override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong())
}

@Serializable          
class ProgrammingLanguage(
    val name: String,
    @Contextual 
    val stableReleaseDate: Date
)

private val module = SerializersModule { 
    contextual(DateAsLongSerializer)
}

val format = Json { serializersModule = module }

fun main() {
    val data = ProgrammingLanguage("Kotlin", SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00"))
    println(format.encodeToString(data))
}

{"name":"Kotlin","stableReleaseDate":1606382352574}

Deriving external serializer for another Kotlin class (exeprimental)

특정 class를 serialize를 하기 위해서 @Serialzable 속성을 해당 class에 붙여 주었으나, primary constructor로 속성을 갖는 class의 경우 이를 외부에서 지정해 줄 수 있습니다.

// NOT @Serializable
class Project(val name: String, val language: String)
                           
@Serializer(forClass = Project::class)
object ProjectSerializer

Project class를 serialize 하기 위해 외부에서 forClass 속성을 이용해 Serializer를 할당해 줍니다.

fun main() {
    val data = Project("kotlinx.serialization", "Kotlin")
    println(Json.encodeToString(ProjectSerializer, data))    
}

 

encode나 decode시에는 직접 serializer를 넘겨주는 형태로 호출하면 정상적으로 Project class를 serialize 할 수 있습니다.

{"name":"kotlinx.serialization","language":"Kotlin"}

External serialization uses properties

Kotlinx serialization 두번째 글에서 @Serializable을 이용하는경우 backing field를 이용한다고 설명했었습니다.

tourspace.tistory.com/360?category=797357

 

[Kotlinx serialization] 기본 사용법#2

이 글은 하기 링크를 기준으로 설명합니다. https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/basic-serialization.md 또한 예제에서 사용된 LogInfo(TAG) { "print log"}의 표현은 Log.d(TAG,..

tourspace.tistory.com

다만 외부에서 serializer를 설정하는경우 backing field에 접근할수 없기 때문에 다른식으로 동작하게 됩니다.

따라서 setter나 primary constructor에 정의된 접근 가능한 property들만 serialize가 가능합니다.

// NOT @Serializable, will use external serializer
class Project(
    // val in a primary constructor -- serialized
    val name: String
) {
    var stars: Int = 0 // property with getter & setter -- serialized
 
    val path: String // getter only -- not serialized
        get() = "kotlin/$name"                                         

    private var locked: Boolean = false // private, not accessible -- not serialized 
}              

@Serializer(forClass = Project::class)
object ProjectSerializer

fun main() {
    val data = Project("kotlinx.serialization").apply { stars = 9000 }
    println(Json.encodeToString(ProjectSerializer, data))
}

Project class에 위와 같이 네개의 속성이 정의되어 있으나 실제 두개만 serialize가 가능합니다.

  • name: primary constructor의 param으로 serialize 가능
  • stars: 일반적인 멤버변수(속성)으로 serialize 가능
  • path: getter만 존재하므로 serialize 불가
  • locked: private 속성으로 외부에서 접근불가 하여 serialize 불가

{"name":"kotlinx.serialization","stars":9000}

반응형