본문으로 바로가기
반응형

이번 포스팅에서는 serializer에 이어서 phlymorphism에 대해서 알아봅니다.

쉽게 얘기해서 상속관계에 있는 serializable class들이 어떻게 encode/decode 되는지에 대한 내용입니다.


 이 내용은 하기 페이지를 기반으로 작성되었습니다.

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

이 글은 여러개의 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/11/27 - [개발이야기/Kotlin] - [Kotlinx serialization] Json 직렬화/역직렬화 - Serializers #4

 

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


Closed polymorphism

상속관계로 이루어진 class들의 serialization를 하기위 해 자식 클래스를 한정하여 사용하는 방법에 대해서 먼저 알아 봅니다.

Static types

Kotlin serialization은 object를 encoding 할때 compile 시점의 type으로 구조가 정해집니다.

@Serializable
open class Project(val name: String)

class OwnedProject(name: String, val owner: String) : Project(name)

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data))
} 

OwnedProject는 open class인 Project를 상속 받습니다.

main() 함수에서 보면 변수 data에 OwnedProject를 넣지만 type을 Project로 강제했기 때문에 결과는 아래와 같습니다.

{"name":"kotlinx.coroutines"}

data의 type을 명시적으로 Project로 지정했기 때문에 kotlin serializer는 Project 라는 고정된 type으로 이를 serialize 합니다.

사실 runtime에 OwnedProject나 또 다른 class (Project를 상속받은)가 들어오도록 코드가 변경되더라도 지정된 type인 Project로만 serialize 됩니다.

이번에는 OwnedProoject의 객체를 부모 type으로 받지 않고 OwnedProject 타입을 그대로 받아 보겠습니다.

@Serializable
open class Project(val name: String)

class OwnedProject(name: String, val owner: String) : Project(name)

fun main() {
    val data = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data))
}  

 이럴경우 OwnedProject@Serializable하지 않기 떄문에 Exception이 발생하게 됩니다.

Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'OwnedProject' is not found. Mark the class as @Serializable or provide the serializer explicitly.

Designing serializable hierarchy

안되는 케이스는 위에서 봤으니, 이제 상속받은 경우에 serialze를 하기 위한 방법을 찾아 봅니다.

가장 간단한 방법으로 OwnedProject에도 @Serializable을 붙일수 있습니다.

하지만 name 변수가 property가 아니기 때문에 constuctor properies requirement 섹션에서 언급한것처럼 compile error가 납니다. ( constuctor properies requirement -> 아래 기본사용법#2 링크 에서 확인 가능합니다.)

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

따라서 property가 아닌 생성자 변수를 property로 만들기 위해서는 부모의 구조를 변경해야 합니다.

부모 class의 해당 변수를 abstract로 만들면, 자식의 변수를 property로 만들 수 있습니다.

또한 부모 class는 abstrat 변수를 포함하기 때문에 abstract class로 변경 됩니다.

@Serializable
abstract class Project {
    abstract val name: String
}

class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data))
}  

정리하면 계층관계에 있는 class가 부모의 멤버변수를 세팅하는 형태라면 serialziable하기 위해서는 부모가 가진 propery는 abstract가 되어야 합니다.

하지만 이렇게 계층구조를 구성한다해도 런타임에서 exception이 발생합니다

Exception in thread "main" kotlinx.serialization.SerializationException: Class 'OwnedProject' is not registered for polymorphic serialization in the scope of 'Project'. Mark the base class as 'sealed' or register the serializer explicitly.

예제에서는 붙이지 않았지만 OwnedProject@Serializable을 붙이더라도 동일한 에러가 발생합니다.

Sealed classes

다형성을 갖는 계층구조의 serialization을 위한 적절한 방법은 부모를 sealed class로 만들고 자식들은 모두 @Serializable을 붙이는 방법입니다. 

@Serializable
sealed class Project {
    abstract val name: String
}
            
@Serializable
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data))
}  

{"type":"example.examplePoly04.OwnedProject","name":"kotlinx.coroutines","owner":"kotlin"}

부모와 자식의 모든 property가 정상적으로 serialize 된걸 확인할 수 있습니다.

이때 type 값은 식별자로써 기본으로 json에 포함되며, 객체의 정보를 표현합니다.

Cusom subclass serial name

결과로 출력되는 type 값은 class 전체의 경로를 보여줍니다. 따라서 이 부분을 수정하려면 SerialName annotation을 해당 class에 붙여 줍니다.

@Serializable
sealed class Project {
    abstract val name: String
}
            
@Serializable         
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data))
}  

{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}

Concrete properties in a base class

base가 되는 sealed class가 backing field를 갖는 다른 property를 가질수도 있습니다.

물론 이 property도 정상적으로 json으로 표현됩니다.

@Serializable
sealed class Project {
    abstract val name: String   
    var status = "open"
}
            
@Serializable   
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val json = Json { encodeDefaults = true } // "status" will be skipped otherwise
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(json.encodeToString(data))
} 

 main()에서 status에 대한 아무런 값을 넣지 않았으므로 encodeDefaults = true 로 해야만 status값도 json에 포함시 킬수 있습니다. (기본값은 출력하지 않으므로)

만약 main()에서 OwnedProject를 만들면서 status에도 따로 값을 할당했다면 encodeDefaults 의 설정 없이도 정상적으로 출력됩니다.

{"type":"owned","status":"open","name":"kotlinx.coroutines","owner":"kotlin"}

물론 여기서 data type을 Project로 명시하지 않아도 (OwnedProject라도) 동일한 결과가 출력됩니다.

Objects

class 뿐만 아니라 object도 sealed class를 상속받을 수 있습니다.

@Serializable
sealed class Response
                      
@Serializable
object EmptyResponse : Response()

@Serializable   
class TextResponse(val text: String) : Response()

fun main() {
    val list = listOf(EmptyResponse, TextResponse("OK"))
    println(Json.encodeToString(list))
}  

다만 동일하게 class든 object든 @Serializable을 붙여야 합니다.

[{"type":"example.examplePoly07.EmptyResponse"},{"type":"example.examplePoly07.TextResponse","text":"OK"}]

object인 EmptyReponse 의 경우 기본적으로 출력되는 type 값만 출력됨을 알수 있습니다.

object이므로 이는 serialize를 하면 아무것도 출력되지 않습니다. (속성을 가지고 있다 하더라도)

이는 세번째 포스팅의 Unit and Singleton object 섹션에서 이미 설명했습니다

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

Open polymorphism

위 예제에서는  부모 class를 sealed class로 만들어 자식들을 한정함으로써 serialization을 가능하도록 합니다.

하지만 여러 open class나 abstract class들은 소스코드 안에서 어디서든 (다른 모듈에서도) subclass를 가질수 있고 따라서 자식 클래스의 목록을 compile time에 다 알수는 없습니다.

다르게 생각해 보면 class의 serialize를 위해서는 해당 class의 serializer가 compile time에 생성되어야 합니다.

하지만 특정 부모를(@Serializable이 붙은) 상속받았다고 하여 annotation processor가 해당 class를 들을 전부 찾아다니며 자식클래스에 대한 모든 serializer를 만드는건 너무나 비효율 적입니다. 실제 serialize를 할지 안할지 모르는데 말이죠.

따라서 이번에는 명시적으로 runtime에 subclass의 목록을 등록하여 사용하는 방법에 대해서 알아봅니다.

Registered subclasses

위에 Designing serializable hierarchy section의 예제로 돌아가 봅니다.

@Serializable
abstract class Project {
    abstract val name: String
}

class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(Json.encodeToString(data))
}  

sealed class를 이용하지 않고 이 코드를 동작시키기 위해서는 SerializersModule{} builder를 이용해서 SerializersModule을 생성해야 합니다. 이 모듈 안에서 polymorphic builder와 subclass 함수로 부모 자식의 관계를 표현합니다.

  • polymorphic : 부모 클래스 표현
  • subclass: 자식 클래스를 표현
val module = SerializersModule { // 모듈 정의
    polymorphic(Project::class) { // 부모 클래스
        subclass(OwnedProject::class) // 자식 클래스
    }
}

// 모듈 정보 전달
val format = Json { serializersModule = module } 

@Serializable
abstract class Project {
    abstract val name: String
}
            
@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(data))
}    

{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}

결과는 sealed class로 했을때와 동일하게 출력 됩니다.

참고로 위 코드는 serializer 함수의 제약사항으로 인하여 JVM에서만 정상 동작합니다. 만약 JS나 Native에서 사용하려면 encodeToString에 명시적으로 serializer를 넣어줘야 합니다. 

format.encodeToString(PolymorphicSerializer(Project::class), data)

안드로이드에서 쓸거라면야 상관 없겠죠?

subclass가 여러개라면 아래와 같이 사용하면 됩니다.(OtherProject를 하나 추가해 봅니다.)

@Serializable
abstract class Project {
    abstract val name: String
}

@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()

@Serializable
@SerialName("other")
class OtherProject(override val name: String, val owner: String) : Project()

fun main() {

    val module = SerializersModule {
        polymorphic(Project::class) {
            subclass(OwnedProject::class)
            subclass(OtherProject::class)
        }
    }

    val format = Json { serializersModule = module }

    val data1 = OwnedProject("kotlinx.coroutines1", "kotlin1")
    val data2 = OtherProject("kotlinx.coroutines2", "kotlin2")
    
    println(format.encodeToString(data1))
    println(format.encodeToString(data2))
}

{"type":"owned","name":"kotlinx.coroutines1","owner":"kotlin1"}

{"type":"other","name":"kotlinx.coroutines2","owner":"kotlin2"}

Serializing interfaces

interface는 polymorphism을 가능하게 하는 형태로 반드시 implement 받은 객체로만 인스턴스화 할수 있습니다.

따라서 super class가 interface인 경우 암묵적으로 PholymorphicSerializer로 serialize할수 있다고 간주 하여 @Serializable 을 붙이지 않아도 정상동작합니다.

val module = SerializersModule {
    polymorphic(Project::class) {
        subclass(OwnedProject::class)
    }
}

val format = Json { serializersModule = module }

interface Project {
    val name: String
}

@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project

fun main() {
    val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(data))
}    

{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}

오히려 interface에 @Serializable을 붙이면 아래와 같은 메시지와 함께 complie error가 납니다.

org.jetbrains.kotlin.codegen.CompilationException: Back-end (JVM) Internal error: @Serializable annotation on class Project would be ignored because it is impossible to serialize it automatically. Provide serializer manually via e.g. companion object

Property of an interface type

interface를 class의 property type으로 갖는 경우에도 위와 같은 사유로 ("Interface는 암묵적으로 polymorphic 하다") 추가적인 정의 없이 사용이 가능합니다.

val module = SerializersModule {
    polymorphic(Project::class) {
        subclass(OwnedProject::class)
    }
}

val format = Json { serializersModule = module }

interface Project {
    val name: String
}

@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project

@Serializable
class Data(val project: Project) // Project is an interface

fun main() {
    val data = Data(OwnedProject("kotlinx.coroutines", "kotlin"))
    println(format.encodeToString(data))
}              

{"project":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}}

Static parent type lookup for polymorphism

polymorphic한 class들을 serialization 할때 root type은 고정됩니다. (위 예제에서는 Project로 고정해 놨습니다.)

만약에 위 예제에서 data 변수의 type을 kotlin 최상의 class인 Any로 받는다면 아래와 같이 exception이 발생합니다.

부모의 부모의 부모의... 다단계 상속관계를 가질 경우라도 SerializersModule에 명시적으로 정해놓은 단계로만 동작한다는 얘기입니다.

fun main() {
    val data: Any = OwnedProject("kotlinx.coroutines", "kotlin")
    println(format.encodeToString(data))
}    

Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found. Mark the class as @Serializable or provide the serializer explicitly.

exception 메시지를 보면 "Any" class의 serializer를 찾을수 없다고 하네요.

그렇다면 SerializerModule의 plymorphic (부모값을 넣는)값을 Any로 변경해 보겠습니다.

val module = SerializersModule {
    polymorphic(Any::class) {
        subclass(OwnedProject::class)
    }
}

 하지만 위와 같이 동일한 exception이 발생합니다.

Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found. Mark the class as @Serializable or provide the serializer explicitly.

에러 메시지를 좀 자세히 (bold 처리한) 읽어보면 "@Serializable을 붙이든지, 아니면 serializer를 명시적으로 제공하라" 라고 나오네요.

Any는 kotlin standard lib에 포함된 최상위 객체로 임의로 개발자가 @Serializable을 붙여줄수 없습니다. 따라서 두번째 방법을 써야 겠네요.

encodeToXxx() 함수의 첫번째 param으로 serializer instance를 넘겨 줍니다.

val module = SerializersModule {
    polymorphic(Any::class) {
        subclass(OwnedProject::class)
    }
}

val format = Json { serializersModule = module }

@Serializable
abstract class Project {
    abstract val name: String
}
            
@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val data: Any = OwnedProject("kotlinx.coroutines", "kotlin")
    
    // serializer 명시적 선언
    println(format.encodeToString(PolymorphicSerializer(Any::class), data))
}    

{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}

이 방법은 Serializer 포스팅 (#4번째 에서)에서 passing a serializer manually section에서 언급된 바 있습니다.

2020/11/27 - [개발이야기/Kotlin] - [Kotlinx serialization] Json 직렬화/역직렬화 - Serializers #4

Explicitly marking prolymorphic class properties

interface를 property로 갖는 class의 경우 다른 추가적인 작업 없이도 serializable 하지만 non serializable한 속성을 갖는다면 이는 serialization이 불가합니다.

이번에는 @Serializable한 class가 non serializable한 속성을 가지는 경우에 대한 처리 방법을 알아봅니다.

일단 앞서 사용했던 Property of an interface type의 예제는 아래와 같습니다.

val module = SerializersModule {
    polymorphic(Project::class) {
        subclass(OwnedProject::class)
    }
}

val format = Json { serializersModule = module }

interface Project {
    val name: String
}

@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project

@Serializable
class Data(val project: Project)

fun main() {
    val data = Data(OwnedProject("kotlinx.coroutines", "kotlin"))
    println(format.encodeToString(data))
}

여기서 Data class의 type은 Project로 이미 interface 이므로 이미 SerializerModule로 정의되어 있어 문제없이 수행됩니다.

또한 SerializerModule에서 부모 자식 관계를 명시해 줬습니다.

하지만 Data class의 project property의 type과 SerializerModule을 아래와 같이 Any로 바꿔 보겠습니다.

val module = SerializersModule {
    polymorphic(Any::class) { // 부모 클래스를 Any로 정의
        subclass(OwnedProject::class)
    }
}

val format = Json { serializersModule = module }

interface Project {
    val name: String
}

@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project

@Serializable
class Data(val project: Any) //Type을 Any로 변경함

fun main() {
    val data = Data(OwnedProject("kotlinx.coroutines", "kotlin"))
    println(format.encodeToString(data))
}

Data class는 serializable 하지만 property인 project의 data type인 Any같은 class는 serialzable하지 않습니다.

#4장에서 serializer를 property에 따로 지정할때 @Serializable(with = [Serializer]) 형태로 사용했었습니다.

아래 링크의 Specifying serializer on a property section에서 확인 가능합니다.

2020/11/27 - [개발이야기/Kotlin] - [Kotlinx serialization] Json 직렬화/역직렬화 - Serializers #4

하지만 이 경우에는 pholymorphism을 이용한다는 의미로 위와는 다른 @Polymorphic annotation을 붙여 줍니다.

val module = SerializersModule {
    polymorphic(Any::class) {
        subclass(OwnedProject::class)
    }
}

...

@Serializable
class Data(@Polymorphic val project: Any) //@Polymorphic 사용

fun main() {
    val data = Data(OwnedProject("kotlinx.coroutines", "kotlin"))
    println(format.encodeToString(data))
}

@Polymorphic을 붙이지 않으면 compile error가 나므로 빌드 타임에 이를 체크할 수 있습니다.

Registering muliple superclasses

위 예제에서 OwnedProject class는 부모 class로 Project 나 Any를 갖도록 수행했었습니다.

이처럼 자식 class가 property로 쓰일때 컴파일 시점에 여러 부모 클래스 타입으로 치환되어야 한다면 SerializersModule에 각각 등록되어야 합니다.

val module = SerializersModule {
    // subclass를 반환하기 위한 extension fun을 Build에 정의함.
    fun PolymorphicModuleBuilder<Project>.registerProjectSubclasses() {
        subclass(OwnedProject::class)
    }
    
    // 각각의 superclass를 등록
    polymorphic(Any::class) { registerProjectSubclasses() }
    polymorphic(Project::class) { registerProjectSubclasses() }
}        

val format = Json { serializersModule = module }

interface Project {
    val name: String
}

@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project

@Serializable
class Data(
    val project: Project,
    @Polymorphic val any: Any 
)

fun main() {
    val project = OwnedProject("kotlinx.coroutines", "kotlin")
    val data = Data(project, project)
    println(format.encodeToString(data))
}

{"project":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"},"any":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}}

Polymorphism and generic classes

Generic을 같는 형태의 pholymorphism을 사용한다면 좀 특별하게 처리를 해야 합니다.

@Serializable
abstract class Response<out T>
            
@Serializable
@SerialName("OkResponse")
data class OkResponse<out T>(val data: T) : Response<T>()

위 예제처럼 Generic값을 갖는 두개의 부모,자식 class가 존재한다고 가정합니다.

Kotlin serialization에서는 다형성 유형을 갖는(자식에서 결정된 T가 부모의 T를 결정하는) OkResponse<T> 의 property를 serialization 할때 Generic type인 T의 대해서 기본적인 구현을 제공하지 않습니다.

따라서 Reponse class에 대한 serializers module을 아래 예시처럼 직접 결정해서 제공해야 합니다.

val responseModule = SerializersModule {
    polymorphic(Response::class) {
        subclass(OkResponse.serializer(PolymorphicSerializer(Any::class)))
    }
}

Reponse에 대한 sub class로 OkResponse의 serializer instance를 넘겨 줍니다.

이때 OkResponse의 serializer는 plugin 에서 생성하는 generic serializer를 이용하고, OkReponse의 T에 대한 type을 명시적으로 지정해야 하므로 PolymorphicSerializer를 이용하여 Any로 등록해 줌으로써 Any 하위 타입(모든타입들)이 data property의 type으로 들어올 수 있도록 합니다.

두개의 module을 합치는 예제에서 실제 사용법을 알아봅니다.

Merging library serializers modules

코드가 많아질수록 여러개의 serializer module 한개에 class 상하 관계를 다 표현하기는 힘듭니다.

코드가 많으면 여기저기로 분산되듯이 module도 분산될 수 있으므로, 여러 serializer module이 존재할때 이를 병합하는 방법에 대해서 알아봅니다.

모듈의 병합은 단순히 + operator를 사용하거나, plus() 함수를 이용합니다.

@Serializable
abstract class Response<out T>

@Serializable
@SerialName("OkResponse")
data class OkResponse<out T>(val data: T) : Response<T>()


@Serializable
abstract class Project {
    abstract val name: String
}

@Serializable
@SerialName("OwnedProject")
data class OwnedProject(override val name: String, val owner: String) : Project()

fun main() {
    val responseModule = SerializersModule {
        polymorphic(Response::class) {
            subclass(OkResponse.serializer(PolymorphicSerializer(Any::class)))
        }
    }

    val projectModule = SerializersModule {
        fun PolymorphicModuleBuilder<Project>.registerProjectSubclasses() {
            subclass(OwnedProject::class)
        }
        polymorphic(Any::class) { registerProjectSubclasses() }
        polymorphic(Project::class) { registerProjectSubclasses() }
    }
  
    // 두개의 모듈 병합
    val format = Json { serializersModule = projectModule + responseModule }

    // both Response and Project are abstract and their concrete subtypes are being serialized
    val data: Response<Project> =  OkResponse(OwnedProject("kotlinx.serialization", "kotlin"))
    val string = format.encodeToString(data)
    println(string)
    println(format.decodeFromString<Response<Project>>(string))
}

{"type":"OkResponse","data":{"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"}} OkResponse(data=OwnedProject(name=kotlinx.serialization, owner=kotlin))

만약 모듈이나 library를 만들어서 제공한다면 제공할때 사용자에게 module을 공개하여 serialize할때 병합하여 사용하도록 가이드 해야 합니다.

 

Default polymorphic type handler for deserialization

Polymorphic한 class를 이용하여 json을 만드는 경우 "type" 을 통해서 객체의 정보를 표현해 줍니다.

만약 deserialization을 할때 아래처럼 type값이 subclass에 등록되지 않은 값(어떤 객체인지 class 정보가 없는)이라면 excpetion이 발생합니다.

fun main() {
    println(format.decodeFromString<Project>("""
        {"type":"unknown","name":"example"}
    """))
}

Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Polymorphic serializer was not found for class discriminator 'unknown'

따라서 subclass에 등록되지 않은 모르는 type으로 들어오는 경우를 처리할 수 있는 기본 handler를 지정할 수 있습니다.

먼저 모르는 type이 있을때  처리할 기본 class를 하나 정의합니다.

@Serializable
abstract class Project {
    abstract val name: String
}

// 기본으로 사용할 모델 class
@Serializable
data class BasicProject(override val name: String, val type: String): Project()

@Serializable
@SerialName("OwnedProject")
data class OwnedProject(override val name: String, val owner: String) : Project()

그리고 아래와 같이 default 함수를 써서 Serialize module에 기본으로 사용할 serializer를 등록해 줍니다.

val module = SerializersModule {
    polymorphic(Project::class) {
        subclass(OwnedProject::class)
        default { BasicProject.serializer() }
    }
}

위와 같이 등록한 경우 type값이 OwnedProject가 아닌 경우는 모두 BasicProject로 deserialize 됩니다.

@Serializable
abstract class Project {
    abstract val name: String
}

@Serializable
data class BasicProject(override val name: String, val type: String): Project()

@Serializable
@SerialName("OwnedProject")
data class OwnedProject(override val name: String, val owner: String) : Project()

val module = SerializersModule {
    polymorphic(Project::class) {
        subclass(OwnedProject::class)
        default { BasicProject.serializer() }
    }
}

val format = Json { serializersModule = module }

fun main() {
    println(format.decodeFromString<List<Project>>("""
        [
            {"type":"unknown","name":"example"},
            {"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"} 
        ]
    """))
}

[BasicProject(name=example, type=unknown), OwnedProject(name=kotlinx.serialization, owner=kotlin)]

사실 위와 같은 코드는 unknown으로 들어오는 들어오는 json의 구조를 알고 있다는 가정하에 defulat값을 지정한 상태입니다.

따라서 실제 코딩할때는 이런 형태가 쓰이기는 어렵습니다. 다만 좀 덜 구조화된 serializer를 custom하게 사용하는 방법을 다음 포스팅인 "JSON features"Maintaining custom JSON attributes section에서 보도록 하겠습니다.

 

결론

이번장은 class들을 Polymorphic하게 사용하는 경우에 대한 방법들에 대해서 설명했습니다.

예제에 등장하는것 코드들은 당연히 정상 동작 하지만, 실제 소스코드에서 내 코드에 맞게 만드려면 많은 시행착오가 필요합니다.

(특히나 Gerneric 부분)

벌써 다섯번째 kotlinx.serialization에 대해서 쓰고 있지만 사실 누구나 이해하기 쉽고, 적용하기 쉽도록 1장 정도 수준에서 사용하는게 가장  바람직하단 생각이 듭니다.

필요에 따라 2장에서 소개하는 여러 plugin serializer의 사용법 정도는 유용하게 적용가능하나, 정말 필요한 경우가 아니라면 4장의 custom serializer를 만들거나, 이번장의 내용처럼 polymorphism이 적용된 serialization은 지양하는게 복잡도를 낮추고, 여러사람이 공통으로 개발하는 부분에서는 낫지 않을까 하는 개인적인 생각입니다.

반응형