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,务必使用带 serializersModuleJson 对象
不支持运行时反射 所有序列化逻辑必须在编译期确定,无法像 Gson 那样动态解析任意类

✅ 最佳实践建议

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

其他替代库 Moshi

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

总结

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

相关推荐
诸神黄昏EX28 分钟前
Android Build系列专题【篇六:VINTF机制】
android
浪客川1 小时前
安卓日志工具类
android
csj501 小时前
安卓基础之《(14)—数据存储(4)应用组件Application》
android
李坤林2 小时前
Android Binder 详解(6) Binder 客户端的创建
android·binder
北京自在科技2 小时前
苹果iOS 26.3实现跨安卓数据无缝迁移
android·ios·findmy
_道隐_2 小时前
Android里面的layer、DisplayList和hardwarebuffer之间是什么关系
android
stevenzqzq4 小时前
ctrl +B和ctrl+shift +B的区别
android·ide·android studio
似霰4 小时前
HIDL Hal 开发笔记5----Same-Process HALs 实例分析
android·framework·hal
robotx4 小时前
安卓16 设置壁纸中应用网格,有两个5X5的选项
android
Yyuanyuxin4 小时前
保姆级学习开发安卓手机软件(三)--安装模拟机并开始简单的进入开发
android·学习