1. 概述

本文将深入探讨 Jackson 在 Kotlin 环境下的使用方式,重点覆盖对象与集合的序列化/反序列化实践,并结合常用注解如 @JsonProperty@JsonInclude 进行配置优化。

对于使用 Kotlin 构建后端服务的开发者来说,Jackson 是最主流的 JSON 处理库之一。但 Kotlin 的语言特性(如数据类、默认参数、不可变属性等)与 Java 存在差异,直接使用原生 Jackson 可能会踩坑 ❌。因此,必须配合 jackson-module-kotlin 才能正确处理 Kotlin 类型系统

✅ 正确集成后,你可以无缝实现:

  • 数据类(data class)到 JSON 的自动映射
  • 默认值字段缺失时的容错反序列化
  • 使用注解自定义字段名和输出策略

下面我们一步步来看如何配置并高效使用。


2. Maven 配置

要让 Jackson 支持 Kotlin,核心是引入官方提供的模块依赖:

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

📌 注意:该模块底层依赖 kotlin-reflect,请确保项目中已包含:

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

最新版本可参考 Maven Central

⚠️ 踩坑提示:如果未注册 KotlinModule 或创建方式不正确,即使加了依赖也可能出现 Cannot construct instance of ... 异常。


3. 对象序列化

我们以一个典型的 Movie 数据类为例:

data class Movie(
    var name: String,
    var studio: String,
    var rating: Float? = 1f
)

创建支持 Kotlin 的 ObjectMapper

有两种推荐方式创建兼容 Kotlin 的 ObjectMapper 实例:

方式一:使用快捷工厂方法 ✅ 推荐新手使用

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper

val mapper = jacksonObjectMapper()

方式二:手动注册 KotlinModule

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule

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

两种方式效果一致,前者更简洁;后者便于后续扩展其他 Module(如 JavaTimeModule)。

序列化示例

@Test
fun whenSerializeMovie_thenSuccess() {
    val movie = Movie("Endgame", "Marvel", 9.2f)
    val serialized = mapper.writeValueAsString(movie)
    
    val json = """
      {
        "name":"Endgame",
        "studio":"Marvel",
        "rating":9.2
      }
    """.trimIndent()

    assertEquals(serialized, json.trimIndent())
}

输出结果为标准 JSON 格式,字段名与属性名一一对应。


4. 对象反序列化

反序列化是从 JSON 字符串重建 Kotlin 对象的过程。

基本用法

@Test
fun whenDeserializeMovie_thenSuccess() {
    val json = """{"name":"Endgame","studio":"Marvel","rating":9.2}"""
    val movie: Movie = mapper.readValue(json)
    
    assertEquals(movie.name, "Endgame")
    assertEquals(movie.studio, "Marvel")
    assertEquals(movie.rating, 9.2f)
}

📌 关键点:**无需显式传入 TypeReference**,只需声明变量类型即可,这是 jackson-module-kotlin 提供的语法糖。

你也可以写成泛型形式:

val movie = mapper.readValue<Movie>(json)

两者等价。

缺失字段处理 —— 利用默认值

当 JSON 中缺少某个字段时,Jackson 会尝试使用类中定义的默认值:

@Test
fun whenDeserializeMovieWithMissingValue_thenUseDefaultValue() {
    val json = """{"name":"Endgame","studio":"Marvel"}""" // 缺少 rating
    val movie: Movie = mapper.readValue(json)
    
    assertEquals(movie.rating, 1f) // 使用了默认值
}

这在接口兼容性升级时非常有用,新增可选字段不影响旧客户端。

4.1 必填字段缺失导致反序列化失败

⚠️ 重点来了:Kotlin 的非空类型(non-null type)不允许为 null。若 JSON 中缺失这些字段,Jackson 将抛出异常。

例如 Movie 中的 namestudio 是非空字段:

@Test
fun whenMissingRequiredParameterOnDeserialize_thenFails() {
    val json = """{"studio":"Marvel","rating":9.2}""" // name 缺失
    val exception = assertThrows<MissingKotlinParameterException> { 
        mapper.readValue<Movie>(json) 
    }
    
    assertEquals("name", exception.parameter.name)
    assertEquals(String::class, exception.parameter.type.classifier)
}

抛出的是 MissingKotlinParameterException,明确指出哪个参数缺失以及其类型信息。

✅ 解决方案:

  • 若字段可为空,改为 String?
  • 或提供默认值:var name: String = "Unknown"

否则必须保证 JSON 包含所有非空构造参数。


5. Map 的序列化与反序列化

Kotlin 中的 Map 结构也能被 Jackson 正确处理。

序列化示例

@Test
fun whenSerializeMap_thenSuccess() {
    val map = mapOf(1 to "one", 2 to "two")
    val serialized = mapper.writeValueAsString(map)
    
    val json = """
      {
        "1":"one",
        "2":"two"
      }
    """.trimIndent()

    assertEquals(serialized, json.trimIndent())
}

注意:Map 的 key 被自动转为字符串。

反序列化要点

反序列化时 必须明确指定泛型类型,否则无法正确还原:

@Test
fun whenDeserializeMap_thenSuccess() {
    val json = """{"1":"one","2":"two"}"""
    val aMap: Map<Int, String> = mapper.readValue(json)
    
    assertEquals(aMap[1], "one")
    assertEquals(aMap[2], "two")
}

❌ 错误示范:

// 不推荐:类型擦除导致运行时问题
val aMap = mapper.readValue(json) as Map<Int, String> // ⚠️ ClassCastException 风险

✅ 正确做法始终是通过变量声明或泛型指定完整类型。


6. 集合(Collection)操作

List、Set 等集合类型的处理逻辑类似。

序列化 List

@Test
fun whenSerializeList_thenSuccess() {
    val movie1 = Movie("Endgame", "Marvel", 9.2f)
    val movie2 = Movie("Shazam", "Warner Bros", 7.6f)
    val movieList = listOf(movie1, movie2)
    val serialized = mapper.writeValueAsString(movieList)
    
    val json = """
      [
        {
          "name":"Endgame",
          "studio":"Marvel",
          "rating":9.2
        },
        {
          "name":"Shazam",
          "studio":"Warner Bros",
          "rating":7.6
        }
      ]
    """.trimIndent()

    assertEquals(serialized, json.trimIndent())
}

反序列化 List

同样需要声明目标类型:

@Test
fun whenDeserializeList_thenSuccess() {
    val json = """[
        {"name":"Endgame","studio":"Marvel","rating":9.2}, 
        {"name":"Shazam","studio":"Warner Bros","rating":7.6}
    ]"""
    
    val movieList: List<Movie> = mapper.readValue(json)
        
    val expected1 = Movie("Endgame", "Marvel", 9.2f)
    val expected2 = Movie("Shazam", "Warner Bros", 7.6f)
    
    assertTrue(movieList.contains(expected1))
    assertTrue(movieList.contains(expected2))
}

⚠️ 踩坑提醒:不要用 ArrayList<Movie> 接收,应优先使用不可变类型 List<Movie>,避免不必要的 mutable 副作用。


7. 修改字段名称 —— @JsonProperty

有时 Java/Kotlin 属性命名规范(驼峰)与 JSON 要求(如小写下划线)不一致,可用 @JsonProperty 映射。

示例:重命名 authorName → author

data class Book(
    var title: String,
    @JsonProperty("author") var authorName: String
)

序列化效果

@Test
fun whenSerializeBook_thenSuccess() {
    val book = Book("Oliver Twist", "Charles Dickens")
    val serialized = mapper.writeValueAsString(book)
    
    val json = """
      {
        "title":"Oliver Twist",
        "author":"Charles Dickens"
      }
    """.trimIndent()

    assertEquals(serialized, json.trimIndent())
}

反序列化也生效

@Test
fun whenDeserializeBook_thenSuccess() {
    val json = """{"title":"Oliver Twist", "author":"Charles Dickens"}"""
    val book: Book = mapper.readValue(json)
    
    assertEquals(book.title, "Oliver Twist")
    assertEquals(book.authorName, "Charles Dickens")
}

✅ 单向映射搞定大小写/命名风格差异,非常实用。


8. 排除空字段 —— @JsonInclude

默认情况下,即使字段为空或为 null,也会出现在序列化结果中。

添加可空字段 genres

data class Book(
    var title: String,
    @JsonProperty("author") var authorName: String
) {
    var genres: List<String>? = emptyList()
}

默认行为:包含空字段

@Test
fun whenSerializeBook_thenSuccess() {
    val book = Book("Oliver Twist", "Charles Dickens")
    val serialized = mapper.writeValueAsString(book)
    
    val json = """
      {
        "title":"Oliver Twist",
        "author":"Charles Dickens",
        "genres":[]
      }
    """.trimIndent()

    assertEquals(serialized, json.trimIndent())
}

这对前端可能造成困扰,比如误判 genres 有内容。

使用 @JsonInclude 控制输出

@JsonInclude(JsonInclude.Include.NON_EMPTY)
data class Book(
    var title: String,
    @JsonProperty("author") var authorName: String
) {
    var genres: List<String>? = emptyList()
}

此时再序列化:

@Test
fun givenJsonInclude_whenSerializeBook_thenEmptyFieldExcluded() {
    val book = Book("Oliver Twist", "Charles Dickens")
    val serialized = mapper.writeValueAsString(book)
    
    val json = """
      {
        "title":"Oliver Twist",
        "author":"Charles Dickens"
      }
    """.trimIndent()

    assertEquals(serialized, json.trimIndent())
}

✅ 效果:genres 因为空列表被排除。

📌 支持的策略还包括:

  • NON_NULL:排除 null 值
  • NON_DEFAULT:排除默认值(如 0、false)
  • ALWAYS:始终包含(默认)

根据业务需求选择合适策略,提升 API 清洁度。


9. 总结

本文系统讲解了 Jackson 在 Kotlin 项目中的正确打开方式:

  • ✅ 必须引入 jackson-module-kotlin 并注册 KotlinModule
  • ✅ 使用 jacksonObjectMapper() 快速构建兼容实例
  • ✅ 数据类支持默认值、非空校验、字段重命名
  • ✅ 集合与 Map 反序列化需显式声明泛型类型
  • ✅ 利用 @JsonProperty 解决命名冲突
  • ✅ 使用 @JsonInclude(NON_EMPTY) 减少冗余输出

📌 最佳实践建议:

  1. 所有 DTO 使用 data class + 默认值增强健壮性
  2. 统一配置全局 ObjectMapper bean,避免重复创建
  3. 生产环境开启 FAIL_ON_UNKNOWN_PROPERTIES = false 防止因字段新增导致解析失败

掌握这些技巧后,你在 Spring Boot 或 Ktor 项目中处理 JSON 将更加得心应手 💪。


原始标题:Jackson Support for Kotlin