You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kotlin中将时间戳转为Java 8 ZonedDateTime时遇值为空问题

问题:Kotlin数据类中createdAt属性始终为空的原因及解决办法

给定的JSON数据

{
  "name": "Hungarian Translation",
  "language": "hu",
  "created_at": 1676618826043
}

目标Kotlin数据类

data class TranslationBook(
    val name: String,
    val language: String,
    val created_at: Long,
    val createdAt: ZonedDateTime? =
        ZonedDateTime.ofInstant(Instant.ofEpochMilli(created_at), ZoneId.of("Asia/Jakarta")),
) : Parcelable

问题原因

核心问题出在JSON解析库的工作逻辑:

  • 无论是Gson、Moshi这类常用解析库,在解析JSON到Kotlin数据类时,只会为JSON中存在的字段(name、language、created_at)赋值。
  • createdAt没有对应的JSON字段,解析库不会触发它的默认值初始化逻辑——默认值仅在你手动创建类实例(如TranslationBook(...))且不传入createdAt参数时才会生效,解析库通过反射实例化对象时不会执行这段默认值计算代码。

解决办法

方案1:用init块手动赋值

把createdAt的转换逻辑放到类的初始化块中,在created_at被解析赋值后自动计算createdAt:

data class TranslationBook(
    val name: String,
    val language: String,
    val created_at: Long,
    var createdAt: ZonedDateTime? = null
) : Parcelable {
    init {
        createdAt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(created_at), ZoneId.of("Asia/Jakarta"))
    }
}

这里将createdAt改为var是因为需要在init块中赋值,若要保持不可变特性,可以用方案2。

方案2:通过构造函数计算(保持不可变)

隐藏created_at的直接暴露,通过次级构造函数完成转换逻辑:

data class TranslationBook(
    val name: String,
    val language: String,
    val createdAt: ZonedDateTime
) : Parcelable {
    @JvmOverloads
    constructor(
        name: String,
        language: String,
        @SerializedName("created_at") created_at: Long
    ) : this(
        name,
        language,
        ZonedDateTime.ofInstant(Instant.ofEpochMilli(created_at), ZoneId.of("Asia/Jakarta"))
    )
}

添加@SerializedName("created_at")是为了让解析库识别JSON中的字段,绑定到次级构造函数的参数上。

方案3:自定义JSON解析适配器(推荐)

如果使用Gson或Moshi,编写自定义适配器直接将created_at字段解析为ZonedDateTime类型,让数据类更简洁:
以Gson的TypeAdapter为例:

class TranslationBookAdapter : TypeAdapter<TranslationBook>() {
    override fun write(out: JsonWriter, value: TranslationBook) {
        // 序列化逻辑(按需实现)
        out.beginObject()
        out.name("name").value(value.name)
        out.name("language").value(value.language)
        out.name("created_at").value(value.createdAt?.toInstant()?.toEpochMilli())
        out.endObject()
    }

    override fun read(`in`: JsonReader): TranslationBook {
        `in`.beginObject()
        var name = ""
        var language = ""
        var createdAt: ZonedDateTime? = null
        while (`in`.hasNext()) {
            when (`in`.nextName()) {
                "name" -> name = `in`.nextString()
                "language" -> language = `in`.nextString()
                "created_at" -> {
                    val timestamp = `in`.nextLong()
                    createdAt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.of("Asia/Jakarta"))
                }
                else -> `in`.skipValue()
            }
        }
        `in`.endObject()
        return TranslationBook(name, language, createdAt!!)
    }
}

注册适配器到Gson:

val gson = GsonBuilder()
    .registerTypeAdapter(TranslationBook::class.java, TranslationBookAdapter())
    .create()

内容的提问来源于stack exchange,提问作者pram

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 02:19:57