다형성 클래스 직렬화하기
다형성 클래스 직렬화하기
다형성(polymorphism)은 공통 인터페이스나 기본 클래스를 통해 서로 다른 타입의 객체를 다룰 수 있게 해 주는 기법이에요. Kotlin 직렬화는 하위 타입 다형성(subtype polymorphism)을 지원해서, 공통으로 선언된 상위 타입(supertype)을 통해 서로 다른 타입을 직렬화할 수 있습니다.
Kotlin에서 인터페이스와 상속이 어떻게 동작하는지 개요를 보려면 인터페이스와 상속을 참고해 주세요.
Kotlin 직렬화는 기본적으로 **정적(static)**이에요. 직렬화되는 값의 정적 타입, 즉 변수의 선언된 타입을 사용해 어떤 프로퍼티를 인코딩할지 결정합니다.
즉 런타임에 값이 하위 클래스의 인스턴스이더라도, 그 정적 타입에 정의된 프로퍼티만 직렬화돼요.
다형성 클래스 계층을 사용하는 예시를 볼게요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
//sampleStart
@Serializable
open class Project(val name: String)
class OwnedProject(name: String, val owner: String) : Project(name)
fun main() {
// Uses Project as the declared static type
val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
// Serializes only properties defined in the static type
println(Json.encodeToString(data))
// {"name":"kotlinx.coroutines"}
}
//sampleEnd
이 예시에서 런타임 값은 OwnedProject 하위 클래스이지만, 선언된 Project 정적 타입의 프로퍼티만 직렬화돼요.
다형성 클래스 계층의 값을 직렬화하려면 Kotlin 직렬화는 두 가지 방법을 제공합니다.
- 닫힌 다형성(closed polymorphism):
sealed기본 클래스나 인터페이스가 모든 하위 클래스를 컴파일 타임에 알 수 있게 보장해요. - 열린 다형성(open polymorphism):
open또는abstract기본 클래스에 하위 클래스를 명시적으로 지정해요.
본문
닫힌 다형성 클래스 직렬화하기
클래스 계층에서 모든 가능한 하위 클래스가 컴파일 타임에 반드시 알려져 있다면 닫힌 다형성을 사용한다고 해요.
이런 보장을 위해 기본 타입으로 sealed class나 sealed interface를 사용하면 돼요. 이를 @Serializable로 표시하고, 모든 하위 클래스도 @Serializable로 표시하면 됩니다.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
//sampleStart
// Defines a sealed class as the base type
@Serializable
sealed class Project {
abstract val name: String
}
// Marks the subclass as serializable
@Serializable
class OwnedProject(override val name: String, val owner: String) : Project()
// Serializes data using the base type as the static type
fun main() {
// Uses the base type as the static type
val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
// A type property is added to identify the serialized subclass
println(Json.encodeToString(data))
// {"type":"OwnedProject","name":"kotlinx.coroutines","owner":"kotlin"}
}
//sampleEnd
기본적으로 JSON 출력의 type 프로퍼티가 클래스 구분자(class discriminator) 역할을 하며 직렬화된 하위 클래스를 식별해요. 이 프로퍼티는 직렬화 중 기본 클래스를 정적 타입으로 사용할 때만 포함됩니다.
클래스 구분자에 다른 키 이름을 쓰도록 JSON을 구성할 수도 있어요. 자세한 내용은 다형성용 클래스 구분자 지정하기 절을 참고해 주세요.
다음은 하위 클래스가 정적 타입이라 type 프로퍼티가 생략되는 예시예요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
//sampleStart
@Serializable
sealed class Project {
abstract val name: String
}
@Serializable
class OwnedProject(override val name: String, val owner: String) : Project()
fun main() {
// The inferred static type is OwnedProject
val data = OwnedProject("kotlinx.coroutines", "kotlin")
// The type property is omitted because the static type is OwnedProject
println(Json.encodeToString(data))
// {"name":"kotlinx.coroutines","owner":"kotlin"}
}
//sampleEnd
객체를 직렬화할 때 기본 타입을 명시적으로 지정하면 출력에 type 프로퍼티가 포함되도록 보장할 수 있어요. encodeToString() 함수에 타입 인자로 기본 타입을 넘기면 됩니다.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
sealed class Project {
abstract val name: String
}
@Serializable
class OwnedProject(override val name: String, val owner: String) : Project()
//sampleStart
fun main() {
// The inferred static type is OwnedProject
val data = OwnedProject("kotlinx.coroutines", "kotlin")
// Specifies the base type explicitly to make use of polymorphism
println(Json.encodeToString<Project>(data))
// {"type":"OwnedProject","name":"kotlinx.coroutines","owner":"kotlin"}
}
//sampleEnd
하위 클래스의 커스텀 직렬 이름 정의하기
기본적으로 Kotlin 직렬화는 닫힌·열린 다형성 계층 모두에서 다형성 하위 클래스의 클래스 구분자 값으로 완전히 정규화된 클래스 이름을 사용해요. 이 값은 클래스를 리팩터링하면 바뀔 수 있죠.
클래스 구분자 값을 안정적으로 유지하려면 @SerialName 어노테이션으로 커스텀 직렬 이름을 정의하면 됩니다.
각 직렬화 가능 클래스는 고유한 @SerialName을 가져야 해요. 별개의 다형성 계층이라도 다른 클래스에 같은 이름을 재사용하면 안 됩니다.
같은 계층 안에서 같은 이름을 재사용하면 인코딩·디코딩 함수가 IllegalStateException을 던져요. 별개의 계층에서 같은 이름을 재사용하면 미묘한 직렬화 문제가 생길 수 있습니다.
@SerialName으로 소스 코드와 무관한 하위 클래스의 안정적인 식별자를 정의할 수 있어요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
//sampleStart
@Serializable
sealed class Project {
abstract val name: String
}
// Assigns a custom serial name to the subclass
@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"}
}
//sampleEnd
backing field를 가진 기본 클래스 프로퍼티
다형성 클래스 계층에서 기본 클래스는 backing field를 가진 프로퍼티를 정의할 수 있고, 이 프로퍼티는 하위 클래스의 프로퍼티와 함께 직렬화돼요. 예를 들면 다음과 같아요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
//sampleStart
@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() {
// Configures a Json instance to encode default values
val json = Json { encodeDefaults = true }
val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
// Serializes base class properties together with subclass properties
println(json.encodeToString(data))
// {"type":"owned","status":"open","name":"kotlinx.coroutines","owner":"kotlin"}
}
//sampleEnd
다형성 클래스 계층에서 객체 직렬화하기
다형성 클래스 계층은 하위 클래스로 객체(object)를 가질 수 있어요. 직렬화 중 객체는 프로퍼티가 없는 클래스와 비슷하게 취급되고, 기본적으로 그 클래스 이름이 type 프로퍼티의 값으로 사용됩니다.
이 객체들을 직렬화에 포함하려면 @Serializable로 표시하면 돼요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
//sampleStart
// Defines a sealed base class
@Serializable
sealed class Response
// Defines an object subclass
@Serializable
object EmptyResponse : Response()
// Defines a class that extends Response
@Serializable
class TextResponse(val text: String) : Response()
// Serializes a list containing different subclasses
fun main() {
val list = listOf(EmptyResponse, TextResponse("OK"))
println(Json.encodeToString(list))
// [{"type":"EmptyResponse"},{"type":"TextResponse","text":"OK"}]
}
//sampleEnd
열린 다형성 클래스 직렬화하기
Kotlin 직렬화는 open·abstract 클래스와 인터페이스에 대한 열린 다형성을 지원해요. 이 모델에서는 하위 클래스를 코드베이스 어디에든, 심지어 다른 모듈에서도 정의할 수 있어요. 컴파일러가 컴파일 타임에 모든 하위 클래스를 판별할 수 없으므로, 하위 클래스를 명시적으로 지정해야 합니다.
모든 하위 클래스가 컴파일 타임에 알려지지 않을 때 열린 다형성을 사용하면 돼요. 런타임에 모든 하위 클래스를 등록하려면 SerializersModule 클래스에 기본 타입과 모든 하위 클래스를 지정해야 합니다.
SerializersModule()빌더 함수로SerializersModule을 만듭니다.SerializersModule안에서polymorphic()함수로 기본 타입을 지정합니다.polymorphic()블록 안에서subclass()함수로 각 하위 클래스를 등록합니다. 하위 클래스를 등록하면 기본 타입과 연결된 하위 클래스 집합에 추가되어 다형성 직렬화·역직렬화에 사용할 수 있게 돼요.serializersModule프로퍼티로SerializersModule을Json인스턴스에 추가합니다.- 선택적으로
@SerialName어노테이션으로 완전히 정규화된 클래스 이름 대신 하위 클래스의 안정적인 식별자를 정의할 수 있어요.
type 프로퍼티를 포함하려면 기본 타입을 정적 타입으로 사용하면 됩니다. 예를 들면 다음과 같아요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
//sampleStart
// Defines a SerializersModule
val module = SerializersModule {
// Specifies Project as the base type
polymorphic(Project::class) {
// Registers OwnedProject as a subclass of Project
subclass(OwnedProject::class)
}
}
// Adds the SerializersModule to a Json instance
val format = Json { serializersModule = module }
// Defines the base type used as the static type during serialization
@Serializable
abstract class Project {
abstract val name: String
}
// Defines a serializable subclass of Project
@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project()
fun main() {
// Uses Project as the static type to make use of polymorphism
val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
println(format.encodeToString(data))
// {"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}
}
//sampleEnd
이 구성은 닫힌 다형성 클래스 직렬화 절의 예시와 같은 JSON 구조를 만들지만, open·abstract 클래스를 지원해요.
이 예시는 serializer() 함수의 제약 때문에 JVM에서만 동작해요. Kotlin/JS와 Kotlin/Native에서는 명시적 직렬화기를 사용해야 합니다: format.encodeToString(PolymorphicSerializer(Project::class), data)처럼요.
이 이슈는 GitHub에서 추적할 수 있어요.
열린 다형성 계층에서 인터페이스 직렬화하기
인터페이스에는 @Serializable을 붙일 수 없어요. Kotlin 직렬화는 인터페이스를 다형성으로 취급하며 기본적으로 직렬화기로 PolymorphicSerializer를 사용합니다.
열린 다형성 직렬화에서 인터페이스를 기본 타입으로 쓰려면, 인터페이스를 구현한 클래스를 @Serializable로 표시하고 SerializersModule에 등록하면 돼요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
//sampleStart
// Defines a SerializersModule
val module = SerializersModule {
polymorphic(Project::class) {
subclass(OwnedProject::class)
}
}
val format = Json { serializersModule = module }
// Defines an interface for polymorphic serialization
interface Project {
val name: String
}
// OwnedProject implements the Project interface
@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project
fun main() {
// Uses the interface for serialization
val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
println(format.encodeToString(data))
// {"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}
}
//sampleEnd
Kotlin/JS와 Kotlin/Native에서는 플랫폼의 제한된 리플렉션(reflection) 능력 때문에 format.encodeToString(PolymorphicSerializer(Project::class), data)로 직렬화기를 명시적으로 지정해야 합니다.
직렬화 가능한 클래스의 프로퍼티로 인터페이스를 사용할 수도 있어요. 이 경우에도 Kotlin 직렬화가 그 프로퍼티에 PolymorphicSerializer를 적용합니다.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
val module = SerializersModule {
polymorphic(Project::class) {
subclass(OwnedProject::class)
}
}
val format = Json { serializersModule = module }
//sampleStart
interface Project {
val name: String
}
@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project
// Defines a serializable class with an interface property
@Serializable
class Data(val project: Project)
fun main() {
val data = Data(OwnedProject("kotlinx.coroutines", "kotlin"))
println(format.encodeToString(data))
// {"project":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}}
}
//sampleEnd
다형성 계층의 기본 타입 선택하기
Kotlin 직렬화는 완전히 정적이에요. 다형성 계층의 기본 타입을 컴파일 타임에 결정합니다. Any를 포함해 어떤 타입이든 명시적으로 구성하기만 하면 기본 타입으로 선택할 수 있어요.
이렇게 하려면:
SerializersModule에 기본 타입과 그 하위 클래스를 지정합니다.- 기본 타입에 자체 직렬화기가 없으면
encodeToString()함수를 호출할 때 그 타입에PolymorphicSerializer를 넘겨줍니다.
예시를 볼게요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
//sampleStart
val module = SerializersModule {
// Registers OwnedProject for the Any base type
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() {
// Uses Any as the static type
val data: Any = OwnedProject("kotlinx.coroutines", "kotlin")
// Specifies a PolymorphicSerializer for the Any base type
println(format.encodeToString(PolymorphicSerializer(Any::class), data))
// {"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}
}
//sampleEnd
PolymorphicSerializer를 제공하지 않으면 다음 예외가 발생해요.
Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found.
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.
같은 하위 클래스에 여러 기본 타입을 사용할 수도 있어요. 단, 각 기본 타입에 대해 하위 클래스를 명시적으로 등록해야 합니다.
각 기본 타입마다 subclass() 호출을 반복하지 않으려면, 하위 클래스를 등록하는 헬퍼 함수를 정의해 각 polymorphic() 블록에서 사용하면 돼요.
다음은 Project와 BaseProject를 모두 기본 타입으로 사용하는 예시예요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
//sampleStart
val module = SerializersModule {
// Defines a helper function that registers subclasses
fun PolymorphicModuleBuilder<Project>.registerProjectSubclasses() {
subclass(OwnedProject::class)
}
// Registers the same subclass for each base type Project and BaseProject
polymorphic(BaseProject::class) { registerProjectSubclasses() }
polymorphic(Project::class) { registerProjectSubclasses() }
}
//sampleEnd
val format = Json { serializersModule = module }
interface BaseProject {
val name: String
}
interface Project : BaseProject
@Serializable
@SerialName("owned")
class OwnedProject(override val name: String, val owner: String) : Project
@Serializable
class Data(
val project: Project,
val baseProject: BaseProject
)
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"},"baseProject":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}}
}
프로퍼티를 다형성으로 직렬화하기
인터페이스와 abstract 클래스는 기본적으로 다형성 직렬화를 사용해요.
Any처럼 직렬화할 수 없는 타입의 프로퍼티를 직렬화하거나, 프로퍼티 타입이 open 클래스일 때는 @Polymorphic 어노테이션으로 프로퍼티를 표시하면 됩니다.
이렇게 하면 그 프로퍼티에 PolymorphicSerializer가 적용돼요.
예시를 볼게요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
val module = SerializersModule {
polymorphic(Any::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
//sampleStart
@Serializable
class Data(
// Applies PolymorphicSerializer to the property
@Polymorphic
val project: Any
)
fun main() {
val data = Data(OwnedProject("kotlinx.coroutines", "kotlin"))
println(format.encodeToString(data))
// {"project":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}}
}
//sampleEnd
open 클래스를 다형성으로 직렬화하기
open 클래스를 다형성으로 직렬화하려면 @Serializable과 @Polymorphic을 둘 다 붙이면 돼요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
val module = SerializersModule {
polymorphic(Project::class) {
subclass(OwnedProject::class)
}
}
val format = Json { serializersModule = module }
//sampleStart
// Applies PolymorphicSerializer to the open class
@Serializable
@Polymorphic
open class Project
@Serializable
@SerialName("owned")
class OwnedProject(val name: String, val owner: String) : Project()
@Serializable
class Data(val project: Project)
fun main() {
val project = OwnedProject("kotlinx.coroutines", "kotlin")
val data = Data(project)
println(format.encodeToString(data))
// {"project":{"type":"owned","name":"kotlinx.coroutines","owner":"kotlin"}}
}
//sampleEnd
sealed 클래스나 인터페이스의 구현 등록하기
같은 계층에서 열린·닫힌 다형성을 조합할 수 있어요. 예를 들어 계층의 루트가 interface이고 여러 하위 계층이 sealed 클래스로 표현되는 경우죠. 각 sealed 하위 클래스를 개별로 등록하는 대신, subclassesOfSealed() 함수로 sealed 클래스나 인터페이스의 모든 구현을 등록할 수 있어요.
이 함수는 하위 클래스가 그 자체로 sealed 직렬화 가능 클래스일 때 하위 클래스를 재귀적으로도 등록해요. 해당 sealed 클래스·인터페이스의 모든 하위 클래스는 구체(concrete)이거나 sealed여야 해요. 그렇지 않으면 IllegalArgumentException이 발생합니다.
예시를 볼게요.
@file:OptIn(ExperimentalSerializationApi::class)
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
interface Base
@Serializable
sealed interface Sub: Base
@Serializable
class Sub1(val data: String): Sub
val module1 = SerializersModule {
polymorphic(Base::class) {
subclassesOfSealed(Sub.serializer())
}
}
val format1 = Json { serializersModule = module1 }
val module2 = SerializersModule {
polymorphic(Base::class) {
// Uses the reified version of subclassesOfSealed() to specify the same sealed type
subclassesOfSealed<Sub>()
}
}
val format2 = Json { serializersModule = module2 }
fun main() {
val data: Base = Sub1("kotlin")
println(format1.encodeToString(data))
// {"type":"Sub1","data":"kotlin"}
println(format2.encodeToString(data))
// {"type":"Sub1","data":"kotlin"}
}
다형성 계층에서 제네릭 하위 타입 직렬화하기
Kotlin 직렬화는 런타임에 제네릭 타입 파라미터의 구체 타입을 자동으로 판별할 수 없어요. 그래서 명시적 구성 없이는 그 파라미터의 직렬화기를 선택할 수 없습니다.
제네릭 다형성 하위 타입을 구성하려면 명시적 직렬화기와 함께 하위 타입을 SerializersModule에 등록하면 돼요. PolymorphicSerializer(Any::class)가 가장 넓은 범위를 가지지만, 제네릭 값이 가질 수 있는 구체 타입을 안다면 더 구체적인 직렬화기를 사용할 수 있어요.
sealed 기본 타입은 이 구성이 필요 없어요. 기본 타입이 자기 타입 인자에 대한 직렬화기를 제공하면 컴파일러 플러그인이 하위 타입 직렬화기를 추론할 수 있기 때문이죠.
예시를 볼게요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
//sampleStart
@Serializable
abstract class Response<out T>
// Defines a generic polymorphic subtype
@Serializable
@SerialName("OkResponse")
data class OkResponse<out T>(val data: T) : Response<T>()
@Serializable
abstract class Project {
abstract val name: String
}
// Defines a subtype used as a generic value
@Serializable
@SerialName("OwnedProject")
data class OwnedProject(override val name: String, val owner: String) : Project()
// Defines serializers for a polymorphic hierarchy with generic subtypes
val responseModule = SerializersModule {
polymorphic(Response::class) {
// Registers the generic subtype
// with a serializer that specifies a PolymorphicSerializer
subclass(OkResponse.serializer(PolymorphicSerializer(Any::class)))
}
polymorphic(Any::class) {
// Registers the subtype used as the generic value
subclass(OwnedProject::class)
}
polymorphic(Project::class) {
// Registers the same subtype for the static base type
subclass(OwnedProject::class)
}
}
// Creates a Json instance with the registered serializers
val format = Json { serializersModule = responseModule }
fun main() {
// Uses a generic polymorphic type with a concrete subtype
val data: Response<Project> = OkResponse(OwnedProject("kotlinx.serialization", "kotlin"))
val jsonString = format.encodeToString(data)
println(jsonString)
// {"type":"OkResponse","data":{"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"}}
val deserializedData = format.decodeFromString<Response<Project>>(jsonString)
println(deserializedData)
// OkResponse(data=OwnedProject(name=kotlinx.serialization, owner=kotlin))
}
//sampleEnd
이 예시에서 PolymorphicSerializer(Any::class)는 제네릭 하위 타입 OkResponse가 Any의 하위 타입으로 다형성 등록된 어떤 값으로든 직렬화되게 해 줘요.
여러 SerializersModule 인스턴스 병합하기
애플리케이션이 커지고 여러 소스 코드 모듈로 나뉘면, 단일 SerializersModule 안에서 모든 클래스 계층을 관리하는 게 어려워질 수 있어요.
plus 연산자로 여러 SerializersModule 인스턴스를 병합해 같은 Json 형식 인스턴스에서 함께 사용할 수 있어요.
예시를 볼게요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
@Serializable
abstract class Response<out T>
@Serializable
data class OkResponse<out T>(val data: T) : Response<T>()
val responseModule = SerializersModule {
polymorphic(Response::class) {
subclass(OkResponse.serializer(PolymorphicSerializer(Any::class)))
}
}
@Serializable
abstract class Project {
abstract val name: String
}
@Serializable
data class OwnedProject(override val name: String, val owner: String) : Project()
val projectModule = SerializersModule {
fun PolymorphicModuleBuilder<Project>.registerProjectSubclasses() {
subclass(OwnedProject::class)
}
polymorphic(Any::class) { registerProjectSubclasses() }
polymorphic(Project::class) { registerProjectSubclasses() }
}
//sampleStart
// Merges the SerializersModule instances from both hierarchies
val format = Json { serializersModule = projectModule + responseModule }
//sampleEnd
fun main() {
val data: Response<Project> = OkResponse(OwnedProject("kotlinx.serialization", "kotlin"))
val string = format.encodeToString(data)
println(string)
// {"type":"OkResponse","data":{"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"}}
println(format.decodeFromString<Response<Project>>(string))
// OkResponse(data=OwnedProject(name=kotlinx.serialization, owner=kotlin))
}
SerializersModule 블록 안에서 모듈을 병합하려면 include() 함수를 사용하면 돼요.
// Merges multiple SerializersModule instances using include()
val combinedModule = SerializersModule {
include(projectModule)
include(responseModule)
}
라이브러리나 공유 모듈의 SerializersModule을 노출하면, 사용자가 이를 자신의 SerializersModule과 병합할 수 있어요.
기본 디직렬화기로 알 수 없는 다형성 하위 타입 역직렬화하기
다형성 데이터를 역직렬화할 때 Kotlin 직렬화는 type 프로퍼티에서 하위 타입을 찾아내요. 하위 타입이 등록되어 있지 않으면 SerializationException과 함께 역직렬화가 실패합니다.
미등록·알 수 없는 다형성 하위 타입을 처리하도록 기본 디직렬화기를 정의할 수 있어요.
다음 예시를 보세요. Project가 기본 타입, OwnedProject가 등록된 하위 타입, BasicProject가 알 수 없는 프로젝트 하위 타입을 나타냅니다.
@Serializable
abstract class Project {
abstract val name: String
}
// Represents unknown project types
@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()
Project의 알 수 없는 다형성 하위 타입을 처리하려면 기본 타입에 대해 기본 디직렬화기를 구성하면 돼요. polymorphic() 블록 안에서 defaultDeserializer() 함수를 사용해 이 미등록 하위 타입에 대한 폴백(fallback)을 정의합니다.
val module = SerializersModule {
polymorphic(Project::class) {
subclass(OwnedProject::class)
defaultDeserializer { BasicProject.serializer() }
}
}
이 SerializersModule 구성을 사용하면 등록된 하위 타입과 미등록된 하위 타입을 모두 역직렬화할 수 있어요.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
@Serializable
abstract class Project {
abstract val name: String
}
// Represents unknown project types
@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()
// Registers a default deserializer for unknown Project subtypes
val module = SerializersModule {
polymorphic(Project::class) {
subclass(OwnedProject::class)
defaultDeserializer { BasicProject.serializer() }
}
}
//sampleStart
val format = Json { serializersModule = module }
fun main() {
// Deserializes both a known and an unknown Project subtype
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)]
}
//sampleEnd
이 예시에서 역직렬화는 하위 타입을 구분하기 위해 type 필드에 의존하지 않고, 대신 BasicProject에 대해 플러그인이 생성한 직렬화기를 사용해요. 이 접근 방식은 알 수 없는 하위 타입이 알려진 구조를 따른다고 가정합니다.
JSON의 경우 알 수 없는 하위 타입이 추가 프로퍼티를 갖되 기본 디직렬화기가 기대하는 구조와 여전히 일치한다면, 형식을 구성해 알 수 없는 키를 무시하도록 할 수도 있어요. 알 수 없는 입력의 구조가 다양하다면 커스텀 직렬화기를 대신 사용하세요.
역직렬화 중 JSON 입력을 더 유연하게 처리하는 법은 JSON 구조 수정하기를 참고해 주세요.
기본 직렬화기로 다형성 타입 직렬화하기
구체 하위 타입을 모두 등록하지 않고도 다형성 기본 타입의 값을 직렬화할 수 있어요. 전체 타입 계층에 접근할 수 없거나 계층이 자주 바뀔 때 유용하죠.
이렇게 하려면:
SerializersModule블록에서polymorphicDefaultSerializer()함수를 사용합니다.polymorphicDefaultSerializer()에 런타임 값에 대한SerializationStrategy를 반환하는 람다를 지정합니다.
CatImpl과 DogImpl이라는 두 private 클래스가 있는 예시를 볼게요. 가시성을 올리지 않기 위해, 공개 인터페이스를 통해 런타임 타입에 기반해 직렬화기를 선택하는 Animal의 기본 직렬화기를 등록합니다.
interface Animal
interface Cat : Animal {
val catType: String
}
interface Dog : Animal {
val dogType: String
}
private class CatImpl : Cat {
override val catType: String = "Tabby"
}
private class DogImpl : Dog {
override val dogType: String = "Husky"
}
object AnimalProvider {
fun createCat(): Cat = CatImpl()
fun createDog(): Dog = DogImpl()
}
// Registers a default serializer for unknown Animal subtypes
val module = SerializersModule {
polymorphicDefaultSerializer(Animal::class) { instance ->
@Suppress("UNCHECKED_CAST")
// Determines the appropriate serializer using a when block
when (instance) {
is Cat -> CatSerializer as SerializationStrategy<Animal>
is Dog -> DogSerializer as SerializationStrategy<Animal>
else -> null
}
}
}
Cat과 Dog용 직렬화기를 정의한 뒤, serializersModule 프로퍼티로 이 SerializersModule을 사용하는 Json 인스턴스를 만들어 Animal 값의 다형성 직렬화를 켜면 됩니다.
// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.modules.*
interface Animal
interface Cat : Animal {
val catType: String
}
interface Dog : Animal {
val dogType: String
}
private class CatImpl : Cat {
override val catType: String = "Tabby"
}
private class DogImpl : Dog {
override val dogType: String = "Husky"
}
object AnimalProvider {
fun createCat(): Cat = CatImpl()
fun createDog(): Dog = DogImpl()
}
// Registers a default serializer for unknown Animal subtypes
val module = SerializersModule {
polymorphicDefaultSerializer(Animal::class) { instance ->
@Suppress("UNCHECKED_CAST")
// Determines the appropriate serializer using a when block based on the runtime value
when (instance) {
is Cat -> CatSerializer as SerializationStrategy<Animal>
is Dog -> DogSerializer as SerializationStrategy<Animal>
else -> null
}
}
}
//sampleStart
// Defines custom serializers
object CatSerializer : SerializationStrategy<Cat> {
override val descriptor = buildClassSerialDescriptor("Cat") {
element<String>("catType")
}
override fun serialize(encoder: Encoder, value: Cat) {
encoder.encodeStructure(descriptor) {
encodeStringElement(descriptor, 0, value.catType)
}
}
}
object DogSerializer : SerializationStrategy<Dog> {
override val descriptor = buildClassSerialDescriptor("Dog") {
element<String>("dogType")
}
override fun serialize(encoder: Encoder, value: Dog) {
encoder.encodeStructure(descriptor) {
encodeStringElement(descriptor, 0, value.dogType)
}
}
}
val format = Json { serializersModule = module }
fun main() {
// Serializes an instance of Cat
println(format.encodeToString<Animal>(AnimalProvider.createCat()))
// {"type":"Cat","catType":"Tabby"}
}
//sampleEnd
더 알아보기
- 생성된 직렬화기를 얻고, 커스텀 직렬화기를 만들고, 직렬화기를 적용하는 방법은 직렬화기 만들고 사용하기에서 배울 수 있어요.