DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
RackNerd;$21.99/年 · 1GB KVM
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

ProtoBuf 解码有效数据报 INVALID(-1),检查可空返回类型

Kotlin ProtoBuf 的可空根类型可能选中 NullableSerializer,把有效消息读成 Unexpected EOF 或 INVALID(-1)。显式解码非空消息,再处理外层缺失。

同一份 Protobuf 字节,解码为普通消息能成功,接收到可空变量后却报 Unexpected EOF,或 Unsupported start group or end group wire type: INVALID(-1)。先检查被推断出来的解码类型,尤其是这一行左边是否为 Box?。

kotlinx.serialization 1.11.0 中,decodeFromByteArray 的泛型入口允许可空类型。当返回目标是 Box? 时,可能取得外层 NullableSerializer;它并不等同于“先按 Box 解码,再把结果赋给一个可空变量”。这个额外的根层解码路径会把原本有效的消息读错,错误文字也可能随字段内容长度变化。

在调用点明确非空消息类型

以下示例使用已启用 serialization 编译插件、依赖 kotlinx-serialization-protobuf 的项目。函数返回类型虽然可容纳 null,调用时仍明确让解码器读取 Box:

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.protobuf.ProtoBuf

@Serializable
data class Box(val value: String)

@OptIn(ExperimentalSerializationApi::class)
fun decodeBox(bytes: ByteArray): Box? {
    return ProtoBuf.decodeFromByteArray<Box>(bytes)
}

关键是显式的 <Box>。函数可以返回 Box?,但这次成功解码的结果是非空 Box;空字节、损坏消息和缺少必需字段仍应按协议失败,不能全部捕获后改成 null。

同样不要为了修复一次推断,把消息类里的所有可空属性都改为非空。Protobuf 字段的缺失、默认值以及可空属性有自己的映射规则,和外层消息本身采用可空 serializer 是两回事。

消息不存在,放在解码之前判断

如果存储或接口用“没有取到字节”表示消息不存在,可以在传输边界区分,而不是要求 Protobuf 字节流表达一个可空根对象:

@OptIn(ExperimentalSerializationApi::class)
fun decodeOptionalBox(bytes: ByteArray?): Box? =
    bytes?.let { ProtoBuf.decodeFromByteArray<Box>(it) }

这段沿用前一个示例的类型和导入,只有真正的 null 字节引用表示不存在。长度为零的 ByteArray 仍交给消息解码;对于必需 value 的 Box,它不应被静默当作“没有消息”。如果业务协议明确允许空消息,应在消息模型中表达相应默认值。

1.12.0 已正式发布,但不能仅凭更新版本号承诺所有可空根类型都受到支持。其正式入口仍接受通用的反序列化策略;为当前调用明确非空消息类型,可以避免依赖返回值推断的差异。

核对时保留相同字节,比较显式 Box 和原先推断调用;同时检查有效短消息、较长字符串、缺失记录与真正损坏的消息。有效消息应完整还原,损坏消息仍要失败。若显式非空类型仍失败,再查字段号、编码格式和数据来源,不应继续套用可空根类型这一原因。