1. 概要

Kotlinでデータクラスを操作する場合、データクラスオブジェクトをMapに変換する必要がある場合があります。 これを支援する組み込みまたはサードパーティのライブラリがいくつかあります。 これらには、Kotlin Reflection、Jackson、Gson、およびKotlinSerializationが含まれます。

この記事では、これらの各方法について説明します。 次に、違いを詳しく見ていきます。

2. データクラス

さまざまな実装に入る前に、サンプルのデータクラスを定義しましょう。

enum class ProjectType {
    APPLICATION, CONSOLE, WEB
}

data class ProjectRepository(val url: String)

data class Project(
    val name: String,
    val type: ProjectType,
    val createdDate: Date,
    val repository: ProjectRepository,
    val deleted: Boolean = false,
    val owner: String?
) {
    var description: String? = null
}

Project には、データクラスに見られる典型的なプロパティがいくつか含まれています。

  • プリミティブ–名前
  • 列挙型–タイプ
  • 日付– createdDate
  • ネストされたデータクラス–リポジトリ
  • デフォルト値のプロパティ–削除済み
  • null許容プロパティ–所有者
  • クラス本体で宣言されたプロパティ– description

Projectのインスタンスを作成します。

val PROJECT = Project(
    name = "test1",
    type = ProjectType.APPLICATION,
    createdDate = Date(1000),
    repository = ProjectRepository(url = "http://test.baeldung.com/test1"),
    owner = null
).apply {
    description = "a new project"
}

PROJECTデータオブジェクトをマップに変換する場合、ソリューションはこれらすべてのプロパティを適切に処理する必要があります。

3. Kotlinリフレクション

Kotlin Reflectionは、最初に試すのが簡単なライブラリです。

3.1. Mavenの依存関係

まず、kotlin-reflect依存関係をpom.xmlに含めましょう。

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>        
    <artifactId>kotlin-reflect</artifactId>
    <version>1.6.10</version>
</dependency>

3.2. すべてのプロパティを使用した変換

リフレクションを使用して、オブジェクトのプロパティをチェックする再帰メソッドを構築できます。 これにより、各プロパティがマップに追加されます。

fun <T : Any> toMap(obj: T): Map<String, Any?> {
    return (obj::class as KClass<T>).memberProperties.associate { prop ->
        prop.name to prop.get(obj)?.let { value ->
            if (value::class.isData) {
                toMap(value)
            } else {
                value
            }
        }
    }
}

次に、変換されたマップを確認しましょう。

assertEquals(
    mapOf(
        "name" to "test1",
        "type" to ProjectType.APPLICATION,
        "createdDate" to Date(1000),
        "repository" to mapOf(
            "url" to "http://test.baeldung.com/test1"
        ),
        "deleted" to false,
        "owner" to null,
        "description" to "a new project"
    ),
    toMap(project)
)

3.3. プライマリコンストラクタのプロパティのみを使用した変換

ご存知のように、データクラスのデフォルトの toString 関数は、データオブジェクトを文字列に変換します。 ただし、この文字列には、プライマリコンストラクタからのプロパティのみが含まれます。

assertFalse(project.toString().contains(Project::description.name))

toMap 関数でこの動作を一致させたい場合は、フィルタリングステップを追加できます。

fun <T : Any> toMapWithOnlyPrimaryConstructorProperties(obj: T): Map<String, Any?> {
    val kClass = obj::class as KClass<T>
    val primaryConstructorPropertyNames = kClass.primaryConstructor?.parameters?.map { it.name } ?: run {
        return toMap(obj)
    }
    return kClass.memberProperties.mapNotNull { prop ->
        prop.name.takeIf { it in primaryConstructorPropertyNames }?.let {
            it to prop.get(obj)?.let { value ->
                if (value::class.isData) {
                    toMap(value)
                } else {
                    value
                }
            }
        }
    }.toMap()
}

この関数では、クラスにプライマリコンストラクターがない場合、元のtoMap関数にフォールバックします。

結果を確認しましょう:

assertEquals(
    mapOf(
        "name" to "test1",
        "type" to ProjectType.APPLICATION,
        "createdDate" to Date(1000),
        "repository" to mapOf(
            "url" to "http://test.baeldung.com/test1"
        ),
        "deleted" to false,
        "owner" to null
    ),
    toMapWithOnlyPrimaryConstructorProperties(project)
)

3.4. 制限事項

このアプローチにはいくつかの制限があります。

  • 特定の形式の文字列への日付プロパティのシリアル化をサポートしていません
  • 結果から特定のプロパティを除外することはできません
  • 変換を元に戻してデータオブジェクトに戻すことはサポートされていません

したがって、これらの機能が必要な場合は、代わりにシリアル化ライブラリを使用する必要があります。

4. ジャクソン

次に、Jacksonライブラリを試してみましょう。

4.1. Mavenの依存関係

いつものように、jackson-module-kotlinの依存関係をpom.xmlに含める必要があります。

<dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-kotlin</artifactId>
    <version>2.11.3</version>
</dependency>

4.2. すべてのプロパティを使用した変換

まず、KotlinModuleを登録したObjectMapperインスタンスを作成しましょう。

val DEFAULT_JACKSON_MAPPER = ObjectMapper().registerModule(KotlinModule())

Jacksonはすべての値をプリミティブ型に変換します。 デフォルトでは、DateLongに変換されます。

assertEquals(
    mapOf(
        "name" to "test1",
        "type" to ProjectType.APPLICATION.name,
        "createdDate" to 1000L,
        "repository" to mapOf(
            "url" to "http://test.baeldung.com/test1"
        ),
        "deleted" to false,
        "owner" to null,
        "description" to "a new project"
    ), DEFAULT_JACKSON_MAPPER.convertValue(PROJECT, Map::class.java)
)

ただし、デフォルトの日付形式を無効にして独自の形式を設定できる場合は、次のようになります。

val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

val JACKSON_MAPPER_WITH_DATE_FORMAT = ObjectMapper().registerModule(KotlinModule()).apply {
    disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
    dateFormat = DATE_FORMAT
}

結果を確認しましょう:

val expected = mapOf(
    "name" to "test1",
    "type" to ProjectType.APPLICATION.name,
    "createdDate" to DATE_FORMAT.format(PROJECT.createdDate),
    "repository" to mapOf(
        "url" to "http://test.baeldung.com/test1"
    ),
    "deleted" to false,
    "owner" to null,
    "description" to "a new project"
)
assertEquals(expected, JACKSON_MAPPER_WITH_DATE_FORMAT.convertValue(PROJECT, Map::class.java))

ジャクソンには他にも多くの便利な機能があることに注意してください。 これらには、日付のシリアル化ヌルフィールドの無視プロパティの無視などが含まれます。

4.3. マップからデータオブジェクトへの変換

JacksonのObjectMapperは、マップをデータオブジェクトに変換して戻すことができます。

assertEquals(PROJECT, JACKSON_MAPPER_WITH_DATE_FORMAT.convertValue(expected, Project::class.java))

ここで、null許容でないプロパティがマップから欠落している場合、JacksonはIllegalArgumentExceptionをスローします。

val mapWithoutCreatedDate = mapOf(
    "name" to "test1",
    "type" to ProjectType.APPLICATION.name,
    "repository" to mapOf(
        "url" to "http://test.baeldung.com/test1"
    ),
    "deleted" to false,
    "owner" to null,
    "description" to "a new project"
)
assertThrows<IllegalArgumentException> { DEFAULT_JACKSON_MAPPER.convertValue(mapWithoutCreatedDate, Project::class.java) }

4.4. 制限事項

JacksonはJSON指向のライブラリです。 したがって、すべての値をプリミティブ型に変換しようとします。 そのため、プロパティオブジェクトをそのままにしておきたい場合は、シナリオに合わない場合があります。 これは、JSONにDateなどのプリミティブ型がないためです。

5. Gson

Gson は、オブジェクトをマップ表現に変換するために使用できる別のライブラリです。

5.1. Mavenの依存関係

まず、gson依存関係をpom.xmlに含める必要があります。

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.5</version>
</dependency>

5.2. すべてのプロパティを使用した変換

同様に、ビルダーを使用してGsonインスタンスを作成しましょう。

val GSON_MAPPER = GsonBuilder().serializeNulls().setDateFormat(DATE_FORMAT).create()

このビルダーを使用して、 null 値でプロパティをシリアル化するようにGsonを設定し、日付形式を指定しました。

結果を確認しましょう:

assertEquals(
    expected,
    GSON_MAPPER.fromJson(GSON_MAPPER.toJson(PROJECT), Map::class.java)
)

Gson は、シリアル化からプロパティを除外するもサポートします。

5.3. マップからデータオブジェクトへの変換

同じマッパーを使用して、データクラスタイプに変換し直すことができます。

assertEquals(
    PROJECT,
    GSON_MAPPER.fromJson(GSON_MAPPER.toJson(expected), Project::class.java)
)

5.4. ヌル不可のプロパティ

ジャクソンとは異なり、Gsonはnull許容でないプロパティが欠落している場合でも文句を言いません。

val newProject = GSON_MAPPER.fromJson(GSON_MAPPER.toJson(mapWithoutCreatedDate), Project::class.java)
assertNull(newProject.createdDate)

これを処理しないと、予期しない例外が発生する可能性があります。

これを修正する1つの方法は、カスタマイズされたものを定義することです。 TypeAdapterFactory。 

class KotlinTypeAdapterFactory : TypeAdapterFactory {
    override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
        val delegate = gson.getDelegateAdapter(this, type)
        if (type.rawType.declaredAnnotations.none { it.annotationClass == Metadata::class }) {
            return null
        }
        return KotlinTypeAdaptor(delegate, type)
    }
}

まず、 KotlinTypeAdapterFactory は、ターゲットクラスがKotlinであるかどうかを確認します。クラスにはkotlinがあります。メタデータアノテーション。 次に、KotlinクラスのKotlinTypeAdapterを返します。

class KotlinTypeAdaptor<T>(private val delegate: TypeAdapter<T>, private val type: TypeToken<T>) : TypeAdapter<T>() {
    override fun write(out: JsonWriter, value: T?) = delegate.write(out, value)

    override fun read(input: JsonReader): T? {
        return delegate.read(input)?.apply {
            Reflection.createKotlinClass(type.rawType).memberProperties.forEach {
                if (!it.returnType.isMarkedNullable && it.get(this) == null) {
                    throw IllegalArgumentException("Value of non-nullable property [${it.name}] cannot be null")
                }
            }
        }
    }
}

デリゲートから結果を返す前に、 KotlinTypeAdaptor は、null許容でないすべてのプロパティが適切な値を持つことを保証します。ここではkotlin-reflectを使用しているため、以前からの依存関係が必要です。

結果を確認しましょう。 まず、KotlinTypeAdapterFactoryを登録してマッパーを作成しましょう。

val KOTLIN_GSON_MAPPER = GsonBuilder()
  .serializeNulls()
  .setDateFormat(DATE_FORMAT)
  .registerTypeAdapterFactory(KotlinTypeAdapterFactory())
  .create()

次に、createdDateプロパティなしでマップを変換してみましょう。

val exception = assertThrows<IllegalArgumentException> {
    KOTLIN_GSON_MAPPER.fromJson(KOTLIN_GSON_MAPPER.toJson(mapWithoutCreatedDate), Project::class.java)
}
assertEquals(
    "Value of non-nullable property [${Project::createdDate.name}] cannot be null",
    exception.message
)

ご覧のとおり、予期されたメッセージで例外がスローされます。

5.5. 制限事項

Jacksonと同様に、GsonもJSON指向のライブラリであり、すべての値をプリミティブ型に変換しようとします。

6. Kotlinのシリアル化

KotlinSerializationはデータシリアル化フレームワークです。 オブジェクトのツリーを文字列に変換し、元に戻します。 これは、Kotlinコンパイラディストリビューションにバンドルされているコンパイラプラグインです。 さらに、Kotlin型システムを完全にサポートおよび実施します。

6.1. Mavenの依存関係

まず、シリアル化プラグインをKotlinコンパイラに追加する必要があります。

<plugin>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-plugin</artifactId>
    <version>1.6.10</version>
    <executions>
        <execution>
            <id>compile</id>
            <phase>compile</phase>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <compilerPlugins>
            <plugin>kotlinx-serialization</plugin>
        </compilerPlugins>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-serialization</artifactId>
            <version>1.6.10</version>
        </dependency>
    </dependencies>
</plugin>

次に、シリアル化ランタイムライブラリの依存関係を追加する必要があります。

<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-serialization-json</artifactId>
    <version>1.3.2</version>
</dependency>

6.2. Kotlinシリアル化アノテーション

Kotlinシリアル化を使用する場合、Dateタイプのシリアライザーを定義する必要があります。

object DateSerializer : KSerializer<Date> {
    override val descriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING)
    override fun serialize(encoder: Encoder, value: Date) = encoder.encodeString(DATE_FORMAT.format(value))
    override fun deserialize(decoder: Decoder): Date = DATE_FORMAT.parse(decoder.decodeString())
}

次に、クラスとプロパティに対応するアノテーションを付ける必要があります。

@Serializable
data class SerializableProjectRepository(val url: String)

@Serializable
data class SerializableProject(
    val name: String,
    val type: ProjectType,
    @Serializable(KotlinSerializationMapHelper.DateSerializer::class) val createdDate: Date,
    val repository: SerializableProjectRepository,
    val deleted: Boolean = false,
    val owner: String?
) {
    var description: String? = null
}

同様のSerializableProjectインスタンスを作成しましょう。

val SERIALIZABLE_PROJECT = SerializableProject(
    name = "test1",
    type = ProjectType.APPLICATION,
    createdDate = Date(1000),
    repository = SerializableProjectRepository(url = "http://test.baeldung.com/test1"),
    owner = null
).apply {
    description = "a new project"
}

6.3. すべてのプロパティを使用した変換

まず、JSONオブジェクトを作成しましょう。

val JSON = Json { encodeDefaults = true }

ご覧のとおり、プロパティ encodeDefaults に設定されています真実。 これは、デフォルト値のプロパティが変換に含まれることを意味します。

最初にデータオブジェクトをJsonObjectに変換してから、JsonObject.toMap関数を呼び出してマップを取得できます。

JSON.encodeToJsonElement(obj).jsonObject.toMap()

ただし、このマップの値はクラス JsonPrimitive のオブジェクトです。したがって、これらをプリミティブ型に変換する必要があります。

inline fun <reified T> toMap(obj: T): Map<String, Any?> {
    return jsonObjectToMap(JSON.encodeToJsonElement(obj).jsonObject)
}

fun jsonObjectToMap(element: JsonObject): Map<String, Any?> {
    return element.entries.associate {
        it.key to extractValue(it.value)
    }
}

private fun extractValue(element: JsonElement): Any? {
    return when (element) {
        is JsonNull -> null
        is JsonPrimitive -> element.content
        is JsonArray -> element.map { extractValue(it) }
        is JsonObject -> jsonObjectToMap(element)
    }
}

次に、結果を確認できます。

val map = KotlinSerializationMapHelper.toMap(SERIALIZABLE_PROJECT)
val expected = mapOf(
    "name" to "test1",
    "type" to ProjectType.APPLICATION.name,
    "createdDate" to MapHelper.DATE_FORMAT.format(SERIALIZABLE_PROJECT.createdDate),
    "repository" to mapOf(
        "url" to "http://test.baeldung.com/test1"
    ),
    "deleted" to false.toString(),
    "owner" to null,
    "description" to "a new project"
)
assertEquals(expected, map)

削除された値も文字列であることに注意してください。これは、JsonElementが常にそのコンテンツを文字列として保存するためです。

さらに、Kotlin Serializationは、アノテーション@kotlinx.serialization.Transient。を使用したプロパティの除外もサポートしています。

6.4. マップからデータオブジェクトへの変換

Json.decodeFromJsonElement は、JsonElementのみをパラメーターとして受け入れます。  変換を逆にすることができます:

val jsonObject = JSON.encodeToJsonElement(SERIALIZABLE_PROJECT).jsonObject
val newProject = JSON.decodeFromJsonElement<SerializableProject>(jsonObject)
assertEquals(SERIALIZABLE_PROJECT, newProject)

6.5. 制限事項

Kotlinシリアル化は文字列指向です。 したがって、元のプロパティ値(booleanまたはDate)が必要なシナリオには適さない場合があります。

7. 結論

この記事では、Kotlinデータオブジェクトをマップに、またはその逆に変換するさまざまな方法を見てきました。 私たちに最も合うものを選ぶべきです。

いつものように、この記事の完全なコードサンプルは、GitHubにあります。