Gson 很好,但在Kotlin上有更合适的序列化工具「Kotlin Serialization」

我们来看看 Gson 的官方介绍:

Gson是一个Java库,可用于将Java对象转换为其JSON表示形式。它还可以用于将JSON字符串转换为等效的Java对象。Gson能够处理任意Java对象,包括那些你没有源代码的现有对象。

Gson 的主要关注点是 Java。在很多情况下,将其与 Kotlin 或 Scala 等其他 JVM 语言一起使用可能效果很好,但不支持特定于语言的特性,例如 Kotlin 的非null类型或带有默认参数的构造函数。这可能会导致令人困惑的错误行为。

使用 Java 以外的语言时,优先选择对该语言有明确支持的 JSON 库。

如果您的应用程序或库可能在Android上运行,请考虑使用 Kotlin序列化或Moshi的代码生成器,它们使用代码生成而非反射。这样可以避免Gson在应用优化时(通常是由于字段缺失或被混淆)出现运行时崩溃,并能在Android设备上带来更快的性能。对于已经熟悉Gson的用户来说,Moshi的API可能更容易上手。如果您仍然想使用Gson并尝试避免这些崩溃,可以在此处了解具体方法。

以上是 Gson官方介绍,上面直接告知用户在 Android 开发 Kotlin 时代不推荐使用 Gson 而应该使用 Kotlin Serialization。但是我估计相当一大部分 Android 开发者并没有看到过这些描述。为什么?因为 Gson 诞生于 Android Java 语言时代,Kotlin 与 Java 兼容,所以在转移到Kotlin语言开发时,旧时代的优秀工具被继承了下来,并且在多数时候使用,并不会有任何问题,所有 Gson 依旧活跃在 Kotlin 时代的 Andorid 开发中。

那么 Gson 在 Kotlin 上有什么问题呢?我们来看一个例子:

kotlin 复制代码
data class Student(val name: String = "", val age: String = "")
json 复制代码
{ "name": "Tom" }

Student类有两个非空字段 val name: String, val age: String, Json 只有一个 name 字段。我们期望如果Json string 没有某个字段则应该使用 Student类字段的默认值,很朴素的期望,对吧?

但实际当你用 Gson 反序列化之后,你会得到一个 name=="Tom" age==null 的 Student 对象,空安全不再安全,同时默认值也一起失效。

那么问题来了,这是为什么呢?

Gson创建因对象类型的不同,分为4种情况:

  • 基础类型、以及基础类型的包装类型等由Gson提供的TypeAdapter通过 new 关键字创建

  • 枚举类型在EnumTypeAdapter中只是通过枚举名称切换不同的枚举常量,不涉及对象的创建

  • 集合和map等容器类型通过Gson内置的对象创建工厂,调用 new 关键字进行创建

  • Java Bean对象的创建比较复杂,分为3种情况,优先级由上到下依次降低:

    • 开发者定义了对象创建工厂InstanceCreator,则使用该工厂创建;
    • 存在默认的无参构造函数,通过反射构造函数创建;
    • 使用Unsafe API 创建。
java 复制代码
#ConstructorConstructor.java
public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
	// 1.获取TypeToken对应的类型(Type)和原始类型(Class<? super T>)
	final Type type = typeToken.getType();
	Class<? super T> rawType = typeToken.getRawType();
	// 2.从instanceCreators中根据类型(Type)获取对应的实例创建器(InstanceCreator)
	//   如果存在,则创建并返回一个新的对象构造器,该构造器使用该实例创建器来创建对象
	final InstanceCreator<T> typeCreator = (InstanceCreator)this.instanceCreators.get(type);
	if (typeCreator != null) {
		return new ObjectConstructor<T>() {
			public T construct() {
				return typeCreator.createInstance(type);
			}
		};
	} else {
		// 3.如果步骤2中没有获取到实例创建器,则尝试根据原始类型(rawType)从instanceCreators中获取实例创建器
		// 如果存在,则创建并返回一个新的对象构造器,该构造器使用该实例创建器来创建对象。
		final InstanceCreator<T> rawTypeCreator = (InstanceCreator)this.instanceCreators.get(rawType);
		if (rawTypeCreator != null) {
			return new ObjectConstructor<T>() {
				public T construct() {
					return rawTypeCreator.createInstance(type);
				}
			};
		} else {
			// 4.如果步骤2和步骤3都没有获取到实例创建器,则尝试调用newDefaultConstructor方法创建一个默认构造器,如果存在,则返回该构造器(无参构造函数)
			ObjectConstructor<T> defaultConstructor = this.newDefaultConstructor(rawType);
			if (defaultConstructor != null) {
				return defaultConstructor;
			} else {
				// 5.如果步骤4中没有创建到默认构造器,则尝试调用newDefaultImplementationConstructor方法创建一个默认实现构造器,如果存在,则返回该构造器(容器类型)
				ObjectConstructor<T> defaultImplementation = this.newDefaultImplementationConstructor(type, rawType);
				// 6.如果步骤5中没有创建到默认实现构造器,则尝试调用newUnsafeAllocator方法创建一个不安全的分配器构造器,最后返回该构造器(Unsafe)
				return defaultImplementation != null ? defaultImplementation : this.newUnsafeAllocator(type, rawType);
			}
		}
	}
}

通过上文我们分析可知,当Java类未提供默认的无参构造函数时,Gson会使用 Unsafe API 来创建对象,这种创建对象的方式不会调用构造函数,因此会导致以下几个可能的问题:

  • 默认值丢失;
  • Kotlin 非空类型失效;
  • 初始化块可能不会正常执行

具体源码阅读可以参考:「赏码」更优雅的使用Gson解析

Kotlin Serialization VS Gson

为了解决此问题,官方推出了 kotlin serialization库,让你的 data class能保持它的特性,不会丢失数据。使用也很简单,只需要加一个注解@Serializable

默认值

kotlin 复制代码
@Serializable data class Student(val name: String, val age: String = "10")

val jsonString = """{ "name": "Tom" }""" 
val objByGson = 
    Gson().fromJson(jsonString, Student::class.java) // 结果 Student(name=Tom, age=null)
val objByKotlinxSerialization = 
    Json.decodeFromString<Student>(jsonString)// 结果 Student(name=Tom, age=10)

可以看到 kotlinx.serialization 正确的处理了默认值,而Gson忽略了默认值

空安全

kotlin 复制代码
@Serializable data class Student(val name: String, val age: String)

val jsonString = """{ "name": "Tom" }""" 
val objByGson = 
    Gson().fromJson(jsonString, Student::class.java) // 结果 Student(name=Tom, age=null)
val objByKotlinxSerialization = 
    Json.decodeFromString<Student>(jsonString)// 结果 抛出异常

为保证 kotlin 的空安全特性,kotlinx.serialization 在反序列化时,如果某个不可空类型的字段在 json 字符串中缺失则会抛出异常:kotlinx.serialization.MissingFieldException: Field 'age' is required for type with serial name 'Student',而Gson 则会序列化成功,但是 data class 的不可空类型定义便被突破,这违背了开发者的期望。这其实是 Gson 在 kotlin 中反序列化遇到最麻烦的问题,在程序运行时,明明定义的类型是不可空,但是为什么会抛出 NullPointerException 呢?

kotlinx.serialization 的这一特性会让我们在声明data class时更加严谨,你需要对字段的可空与否有自己的一番思考 ------ 这显然比无脑将字段声明为可空类型要好的多得多。当然,如果你还是担心由此引发的崩溃问题,同时又不想后续的使用上加上一堆?.的可空调用判断,你可以给其赋上默认值:

kotlin 复制代码
@Serializable data class Student(val name: String, val age: String)

序列化三方库

不少同学看到这边就想到一点,如果使用注解,那如何学历噩耗三方库的类呢?是个好问题,这确实是 Kotlinx.serialization 不如 Gson 方便的地方。不过官方也提供了相关支持,相对而言麻烦了点。 以下是AI给出的答案:


🌰 示例:序列化 java.time.LocalDateTime

由于你不能给第三方类加 @Serializable,必须:

  1. 为该类型编写一个 KSerializer<T> 实现

  2. 在使用处显式指定这个 serializer (通过 @Serializable(with = YourSerializer::class))

    • 或通过 SerializersModule 全局注册为 contextual serializer

假设你要序列化 JDK8 的 LocalDateTime(你无法修改它)。

步骤 1:编写 KSerializer
复制代码
Kotlin

💡 使用 object 单例,避免重复创建。


步骤 2:在你的数据类中使用(显式绑定)
Kotlin 复制代码
object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
    override val descriptor = PrimitiveSerialDescriptor("LocalDateTime", kotlinx.serialization.descriptors.PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: LocalDateTime) {
        encoder.encodeString(value.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
    }

    override fun deserialize(decoder: Decoder): LocalDateTime {
        return LocalDateTime.parse(decoder.decodeString(), DateTimeFormatter.ISO_LOCAL_DATE_TIME)
    }
}

✅ 这样就能正常编解码:

Kotlin 复制代码
val event = Event("Meeting", LocalDateTime.of(2025, 1, 10, 14, 30))
val json = Json.encodeToString(event)
// {"title":"Meeting","timestamp":"2025-01-10T14:30"}

val decoded = Json.decodeFromString<Event>(json)

🔁 替代方案:全局注册为 Contextual Serializer

如果你多处使用 LocalDateTime ,不想每个字段都写 @Serializable(with = ...),可以全局注册:

步骤 1:创建 SerializersModule
Kotlin 复制代码
val appModule = SerializersModule {
    contextual(LocalDateTime::class, LocalDateTimeSerializer)
}
步骤 2:构建带 module 的 Json 实例
Kotlin 复制代码
val JsonWithTime = Json {
    serializersModule = appModule
    // 可选:ignoreUnknownKeys = true 等
}
步骤 3:在数据类中不要加 with,直接使用类型
Kotlin 复制代码
@Serializable
data class Event(
    val title: String,
    val timestamp: LocalDateTime  // ✅ 自动使用 contextual serializer
)
使用时必须用配置好的 Json 实例:
ini 复制代码
Kotlin
编辑
1val jsonStr = JsonWithTime.encodeToString(event)
2val event2 = JsonWithTime.decodeFromString<Event>(jsonStr)

⚠️ 注意:如果使用默认的 Json.encodeToString()(无 module),会报错:
Serializer for class 'LocalDateTime' is not found.


❗ 重要限制与注意事项

问题 说明
不能自动发现 kotlinx.serialization 不会自动为未注解的类型生成 serializer,必须显式提供
泛型支持需小心 对于 List<Point>,你需要: ListSerializer(PointSerializer) 或在 module 中注册 Point 后使用 serializer<List<Point>>()
必须用正确的 Json 实例 如果用了 contextual,务必使用带 serializersModule 的 Json 对象
不支持运行时反射 所有序列化逻辑必须在编译期确定,无法像 Gson 那样动态解析任意类

✅ 最佳实践建议

  1. 优先使用 @Serializable(with = ...)
    适用于局部、明确的场景,代码清晰。
  2. 多处使用 → 用 SerializersModule + contextual
    避免重复注解,但要确保所有编解码都走同一个 Json 实例。
  3. 为常用类型封装工具
    比如创建 CommonSerializers.kt 集中管理 LocalDateTime、Uri、UUID 等。
  4. 避免直接序列化第三方类
    更安全的做法:创建自己的 @Serializable DTO,内部转换第三方类(解耦 + 控制字段)。

其他替代库 Moshi

Moshi 也是官方推荐的 kotlin 序列化工具库,本文就不详细介绍了,想了解的同学可以访问Moshi,其来自于大名鼎鼎的 Suqare 开源。

总结

工具就是工具,不一定是最完美的,我们需要根据当前项目需求选择合适开发者自己的工具,不是说一定要用哪个。很显然,如果你的项目Gson 已经存在了很久,想现在替换掉也不现实。但如果你在当前的 Kotlin 项目中使用 Gson,你必须要知道当前存在的风险点,并去规避它。如果你需要在新项目中序列化,那么毫不犹豫,Kotlinx.serialization 或者 Moshi 是你更好的选择!

相关推荐
千里马学框架5 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台5 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone5 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc5 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo5 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077005 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work5 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼5 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work5 天前
ch21 签名、校验与发版
kotlin
AFinalStone5 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui