Skip to content

序列化

序列化用于在仓颉对象和 JSON 之间做双向转换。

在 Spire 中,这部分能力来自 soulsoft_serialization。它的核心做法是:

  • @Serialization 宏在编译期为类补齐序列化和反序列化代码
  • JsonSerializer 统一处理 JSON 字符串和 DataModel
  • JsonSerializerOptions 控制严格模式、数字字符串、日期格式和 null 写出行为
  • JsonConverter<T> 覆盖某个类型的默认读写逻辑

最小示例

如果你只是想先把对象转成 JSON,再从 JSON 读回来,可以从这个例子开始:

cangjie
import soulsoft_serialization.*
import soulsoft_serialization.macros.*

@Serialization
class Student {
    // 以下划线开头的 var 字段会被序列化宏处理。
    private var _id: Int64 = 0
    private var _name: String = ""
    private var _optAge: ?Int64 = None
}

main(): Int64 {
    // 使用宏生成的构造参数创建对象。
    let student = Student(id: 1, name: "alice", optAge: None)

    // 把对象序列化成 JSON 字符串。
    let json = JsonSerializer.serializeObject(student)
    println(json)

    // 再从 JSON 字符串反序列化回对象。
    let restored = JsonSerializer.deserializeObject<Student>(json)
    println(restored.name)
    return 0
}

这个例子里最重要的点只有 3 个:

  • 类上用了 @Serialization
  • 参与序列化的字段都写成 _field: Type = defaultValue
  • 实际读写 JSON 时用的是 JsonSerializer

字段约定

@Serialization 只会处理满足下面规则的成员变量:

  • 字段名以下划线开头,比如 _id
  • 必须是 var,不能是 let
  • 必须显式写出类型

例如:

cangjie
@Serialization
class User {
    // 满足规则的字段会自动生成同名公开属性。
    private var _id: Int64 = 0
    private var _name: String = ""
    private var _optAge: ?Int64 = None
}

下面这些写法不会被宏处理:

cangjie
// 字段名没有以下划线开头,不会参与宏处理。
private var id: Int64 = 0
// let 字段不会被序列化宏处理。
private let _id: Int64 = 0
// 没有显式类型的字段也不会被宏处理。
private var _id = 0

宏会生成什么

对符合规则的字段,宏会自动补齐几类成员:

  • 一个实现了 ISerialization<T> 的类型声明
  • serializeObject(options)deserializeObject(dm, options)
  • 同名公开属性,属性名会去掉字段前面的下划线
  • 如果类里没有无参构造器,会额外生成一个按字段展开的构造器

例如 _name 会生成 name 属性;_optAge 会生成 optAge 属性。

如果字段本身是 var,生成的属性会带 setter。这也是为什么示例里的对象既可以用构造器初始化,也可以在后续直接赋值。

自定义属性

如果你已经手写了同名属性,宏不会再重复生成。

这适合把内部字段暴露成更宽的只读视图:

cangjie
import std.collection.*
import soulsoft_serialization.*
import soulsoft_serialization.macros.*

@Serialization
class Order {
    // 宏会处理底层字段,但这里手动暴露只读属性。
    private var _items: ArrayList<String> = ArrayList<String>()

    public prop items: ArrayList<String> {
        get() {
            _items
        }
    }
}

字段名映射

默认情况下,JSON 键名就是字段去掉下划线后的名字。

如果 JSON 用的是别的名字,可以加 @DataField

cangjie
import soulsoft_serialization.*
import soulsoft_serialization.macros.*

@Serialization
class User {
    private var _id: Int64 = 0

    // 把 user_name 映射到 _userName 字段。
    @DataField["user_name"]
    private var _userName: String = ""

    // 把 create_time 映射到 _createTime 字段。
    @DataField["create_time"]
    private var _createTime: String = ""
}

这样序列化时输出的是 user_namecreate_time,反序列化时也会按这两个键名回填。

继承

如果子类也需要把父类字段一起纳入序列化,可以在宏参数里显式声明父类:

cangjie
import soulsoft_serialization.*
import soulsoft_serialization.macros.*

@Serialization
open class BaseEntity {
    // 父类字段也可以参与序列化。
    private var _id: Int64 = 0
}

@Serialization[superType: BaseEntity]
class Article {
    // 子类自己的字段会和父类字段一起处理。
    private var _title: String = ""
    public init() {}
}

按当前实现,子类序列化时会先追加父类字段,再追加自己的字段;反序列化时也会先处理父类部分。

核心 API

最常用的是 JsonSerializer

方法作用
JsonSerializer.serializeObject(data)用默认选项把对象序列化成 JSON 字符串
JsonSerializer.serializeObject(data, options)用指定选项序列化
JsonSerializer.deserializeObject<T>(json)从 JSON 字符串反序列化
JsonSerializer.deserializeObject<T>(json, options)用指定选项反序列化
JsonSerializer.deserializeObject<T>(dm)DataModel 反序列化
JsonSerializer.deserializeObject<T>(dm, options)DataModel 按指定选项反序列化
JsonSerializer.deserializeObject(typeInfo, json, options)通过 TypeInfo 动态反序列化,返回 Any
JsonSerializer.deserializeObject(typeInfo, dm, options)通过 TypeInfoDataModel 动态反序列化

如果你已经拿到了对象,也可以直接调用实例方法:

cangjie
// 创建序列化选项对象。
let options = JsonSerializerOptions()
// 构造待序列化对象。
let student = Student(id: 1, name: "alice", optAge: None)
// 先把对象写成 DataModel。
let dm = student.serializeObject(options)
// 再从 DataModel 还原对象。
let restored = Student.deserializeObject(dm, options)

选项控制

JsonSerializerOptions 有 4 个最关键的开关:

选项默认值作用
nullableJsonNullable.Required控制非可空字段缺失或为 null 时是否报错
numberHandlingJsonNumberHandling.Strict控制数字能否从字符串读取,以及写出时是否转成字符串
defaultIgnoreConditionJsonIgnoreCondition.Never控制序列化时是否忽略 None 字段
dateFormatStringNone自定义 DateTime 的读写格式

nullable

默认是 JsonNullable.Required

这意味着:

  • 非可空字段缺失,会抛 DataModelException
  • 非可空字段值为 null,也会抛 DataModelException
  • 可空字段 ?T 即使缺失或为 null,也会保留为 None

如果改成 JsonNullable.Disabled,非可空字段在缺失或为 null 时不会报错,而是保留字段声明时的默认值。

cangjie
// 调整 nullable 策略,让缺失的非可空字段回退到默认值。
let options = JsonSerializerOptions()
options.nullable = JsonNullable.Disabled

let json = #"{"name": "alice"}"#
// 用自定义选项执行反序列化。
let student = JsonSerializer.deserializeObject<Student>(json, options)

这里如果 Student.id 是非可空字段,那么它会保留默认值 0,而不是抛异常。

numberHandling

这个选项有两个常用标记:

  • JsonNumberHandling.AllowReadingFromString
  • JsonNumberHandling.WriteAsString

可以单独使用,也可以用 | 组合。

cangjie
// 允许数字从字符串读取,并在写出时转成字符串。
let options = JsonSerializerOptions()
options.numberHandling =
    JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString

按当前实现:

  • AllowReadingFromString 允许把 "42" 读成整数,把 "3.14" 读成浮点数
  • 对非可空数字字段,空字符串在严格模式下会报错,在 Disabled 模式下会回退到默认值
  • WriteAsString 会把整数、浮点和 Decimal 写成 JSON 字符串

defaultIgnoreCondition

当它被设为 JsonIgnoreCondition.WhenWritingNull 时,值为 None 的可空字段不会写入输出 JSON。

cangjie
// 序列化时忽略值为 None 的可空字段。
let options = JsonSerializerOptions()
options.defaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull

这个开关只影响写出,不影响读取。

dateFormatString

如果字段里有 DateTime,可以用它指定格式:

cangjie
import std.time.*
import soulsoft_serialization.*

// 设置 DateTime 的统一格式。
let options = JsonSerializerOptions()
options.dateFormatString = "yyyy-MM-dd HH:mm:ss"

设置后:

  • 序列化会按这个格式输出字符串
  • 反序列化也会按同样的格式解析

自定义转换器

如果某个类型的默认行为不符合你的要求,可以继承 JsonConverter<T>

cangjie
import soulsoft_serialization.*

class UpperCaseStringConverter <: JsonConverter<String> {
    public func read(dm: DataModel, options: JsonSerializerOptions): String {
        // 读取时把字符串统一转成大写。
        match (dm) {
            case value: DataModelString => value.getValue().toAsciiUpper()
            case _ => throw DataModelException("expected String")
        }
    }

    public func write(value: String, options: JsonSerializerOptions): DataModel {
        // 写出时同样把字符串转成大写。
        return value.toAsciiUpper().serialize()
    }
}

// 把自定义转换器加入选项列表。
let options = JsonSerializerOptions()
options.converters.add(UpperCaseStringConverter())

然后这个 options 参与的序列化和反序列化都会优先使用这个转换器。

这里有两个行为值得记住:

  • 自定义转换器优先于内置转换器
  • 同一个类型注册多个转换器时,后注册的优先级更高

支持的类型

按当前源码和单测,框架内置支持下面这些常见类型:

  • 整数:Int8Int16Int32Int64UInt8UInt16UInt32UInt64
  • 浮点:Float16Float32Float64
  • 其他基础类型:BoolString
  • 数值与时间:BigIntDecimalDateTime
  • 可空类型:?T / Option<T>
  • 集合:Array<T>ArrayList<T>HashSet<T>HashMap<K, V>
  • JSON 原始结构:JsonValue

其中有几个容易忽略的点:

  • BigInt 默认写成 JSON 字符串
  • Decimal 默认写成 JSON 数字;如果启用 WriteAsString,会改成字符串
  • JsonValue 会直接内嵌到最终 JSON 中,而不是再包一层字符串

如果你在集合里放的是自定义类型,那么元素类型本身也要能参与 ISerialization<T>

集合与 JsonValue

集合字段可以直接放进被 @Serialization 标记的类里:

cangjie
import std.collection.*
import soulsoft_serialization.*
import soulsoft_serialization.macros.*

@Serialization
class Catalog {
    // 数组字段可以直接参与序列化。
    private var _tags: Array<String> = []
    // ArrayList 也会按集合处理。
    private var _scores: ArrayList<Int64> = ArrayList<Int64>()
    // HashMap 会写成 JSON 对象结构。
    private var _meta: HashMap<String, String> = HashMap<String, String>()
}

嵌套集合也可以工作,比如 Array<Array<Int64>>HashMap<String, Array<String>>

如果你需要保留一段原始 JSON 结构,可以直接使用 JsonValue

cangjie
import stdx.encoding.json.*
import soulsoft_serialization.*
import soulsoft_serialization.macros.*

@Serialization
class PayloadModel {
    // 直接保存一段原始 JSON 结构。
    private var _payload: JsonValue = JsonValue.fromStr("{}")
    // 可空 JsonValue 缺失时保持 None。
    private var _optPayload: ?JsonValue = None
}

运行时行为

根据当前实现和单测,有几条行为很值得提前知道:

  • 反序列化时,多余的未知字段会被忽略
  • 对象反序列化要求输入是 JSON 对象;如果传入数组、字符串或其他基本值,会抛 DataModelException
  • 非可空字段报错时,异常消息会带上字段名
  • 如果类里手写了无参构造器,宏不会再为它生成展开字段的构造器

使用建议

  • 先把字段写成 _field: Type = defaultValue 的固定格式,再加 @Serialization
  • 默认先用严格模式;只有明确要兼容缺失字段或 null 时,再切到 JsonNullable.Disabled
  • 面向前端接口时,如果后端经常收到数字字符串,再开启 AllowReadingFromString
  • 当 JSON 键名和仓颉属性名不一致时,优先用 @DataField,不要在业务代码里手工搬运
  • 如果只是个别类型需要特殊读写,优先写 JsonConverter<T>,不要整体绕开 JsonSerializer