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)
)
注意:Boolean
和 Date
都会被转换为字符串。
6.4. 反向转换
支持从 Map 转换回对象:
val jsonObject = JSON.encodeToJsonElement(SERIALIZABLE_PROJECT).jsonObject
val newProject = JSON.decodeFromJsonElement<SerializableProject>(jsonObject)
assertEquals(SERIALIZABLE_PROJECT, newProject)
6.5. 局限性
- ❌ 无法保留原始类型(如
Date
、Boolean
) - ⚠️ 适用于字符串序列化,不适用于保持对象结构的场景
7. 总结
方法 | 支持类型转换 | 支持反向转换 | 支持字段控制 | 是否保留原始类型 |
---|---|---|---|---|
Kotlin Reflection | ✅ | ❌ | ❌ | ✅ |
Jackson | ✅ | ✅ | ✅ | ❌ |
Gson | ✅ | ✅ | ✅ | ❌ |
Kotlin Serialization | ✅ | ✅ | ✅ | ❌ |
✅ 推荐使用 Jackson 或 Kotlin Serialization:支持双向转换、字段控制等高级功能。
⚠️ Kotlin Reflection 适合简单场景:不需要序列化、仅需 Map 结构时使用。
完整示例代码请参考 GitHub 仓库。