1. 概述

在 Kotlin 中使用 data class 时,有时我们需要将其对象转换为 Map。我们可以借助 Kotlin 自带的反射机制,也可以使用第三方库如 Jackson、Gson、Kotlin Serialization 等来实现这一需求。

本文将介绍几种常见的转换方式,并对比它们的优缺点,帮助你根据实际场景选择最合适的方案。

2. 示例 Data Class

我们先定义一个典型的 data class,以便后续测试转换逻辑。

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
}

该类包含以下几种常见属性类型:

  • 基本类型:name
  • 枚举类型:type
  • 日期类型:createdDate
  • 嵌套 data class:repository
  • 有默认值的属性:deleted
  • 可空属性:owner
  • 在类体中声明的属性:description

我们创建一个实例用于后续测试:

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"
}

3. Kotlin Reflection

Kotlin 提供了反射库 kotlin-reflect,我们可以基于它手动实现一个递归转换函数。

3.1. Maven 依赖

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

3.2. 转换所有属性

我们可以编写一个递归函数将对象转换为嵌套的 Map<String, Any?>

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() 的行为),可以增加过滤逻辑:

fun <T : Any> toMapWithOnlyPrimaryConstructorProperties(obj: T): Map<String, Any?> {
    val kClass = obj::class as KClass<T>
    val primaryConstructorPropertyNames = kClass.primaryConstructor?.parameters?.map { it.name } ?: 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()
}

测试结果如下:

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. 局限性

  • ❌ 无法格式化 Date 类型
  • ❌ 无法排除某些字段
  • ❌ 不支持反向转换为对象

如果需要这些功能,建议考虑使用序列化库。

4. Jackson

Jackson 是一个广泛使用的 JSON 序列化库,也可以用于将对象转换为 Map。

4.1. Maven 依赖

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

4.2. 转换所有属性

创建一个 ObjectMapper 实例:

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

默认情况下,Date 会被转为时间戳(Long):

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
}

4.3. 反向转换

Jackson 支持从 Map 转换回对象:

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

⚠️ 如果 Map 中缺少非空字段,Jackson 会抛出 IllegalArgumentException

4.4. 局限性

  • ❌ 会将所有值转为 JSON 原始类型(如 Date -> Long
  • ❌ 无法保留原始对象结构

5. Gson

Gson 是另一个流行的 JSON 库,同样可以用于对象到 Map 的转换。

5.1. Maven 依赖

<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()

测试转换:

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

5.3. 反向转换

同样支持反向转换:

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

⚠️ Gson 不会检查非空字段是否缺失,可能导致运行时异常。

5.4. 强制非空字段检查

我们可以自定义 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)
    }
}

然后注册该工厂:

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

测试缺失字段时是否会抛出异常:

assertThrows<IllegalArgumentException> {
    KOTLIN_GSON_MAPPER.fromJson(KOTLIN_GSON_MAPPER.toJson(mapWithoutCreatedDate), Project::class.java)
}

5.5. 局限性

  • ❌ 与 Jackson 类似,会将对象转换为原始类型
  • ⚠️ 默认不检查非空字段缺失

6. Kotlin Serialization

Kotlin Serialization 是 Kotlin 官方提供的序列化框架,支持将对象转换为 JSON 或 Map。

6.1. Maven 依赖

<plugin>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-plugin</artifactId>
    <version>1.6.10</version>
    <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. 注解与序列化类

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 注解:

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

6.3. 转换为 Map

构建 Json 实例并实现转换逻辑:

val JSON = Json { encodeDefaults = true }

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)
    }
}

测试结果:

assertEquals(
    mapOf(
        "name" to "test1",
        "type" to "APPLICATION",
        "createdDate" to "1970-01-01 08:00:01",
        "repository" to mapOf("url" to "http://test.baeldung.com/test1"),
        "deleted" to "false",
        "owner" to null,
        "description" to "a new project"
    ),
    toMap(SERIALIZABLE_PROJECT)
)

注意:BooleanDate 都会被转换为字符串。

6.4. 反向转换

支持从 Map 转换回对象:

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

6.5. 局限性

  • ❌ 无法保留原始类型(如 DateBoolean
  • ⚠️ 适用于字符串序列化,不适用于保持对象结构的场景

7. 总结

方法 支持类型转换 支持反向转换 支持字段控制 是否保留原始类型
Kotlin Reflection
Jackson
Gson
Kotlin Serialization

推荐使用 Jackson 或 Kotlin Serialization:支持双向转换、字段控制等高级功能。
⚠️ Kotlin Reflection 适合简单场景:不需要序列化、仅需 Map 结构时使用。

完整示例代码请参考 GitHub 仓库


原始标题:Convert a Kotlin Data Class Object to a Map