序列化
序列化用于在仓颉对象和 JSON 之间做双向转换。
在 Spire 中,这部分能力来自 soulsoft_serialization。它的核心做法是:
- 用
@Serialization宏在编译期为类补齐序列化和反序列化代码 - 用
JsonSerializer统一处理 JSON 字符串和DataModel - 用
JsonSerializerOptions控制严格模式、数字字符串、日期格式和null写出行为 - 用
JsonConverter<T>覆盖某个类型的默认读写逻辑
最小示例
如果你只是想先把对象转成 JSON,再从 JSON 读回来,可以从这个例子开始:
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 - 必须显式写出类型
例如:
@Serialization
class User {
// 满足规则的字段会自动生成同名公开属性。
private var _id: Int64 = 0
private var _name: String = ""
private var _optAge: ?Int64 = None
}下面这些写法不会被宏处理:
// 字段名没有以下划线开头,不会参与宏处理。
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。这也是为什么示例里的对象既可以用构造器初始化,也可以在后续直接赋值。
自定义属性
如果你已经手写了同名属性,宏不会再重复生成。
这适合把内部字段暴露成更宽的只读视图:
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:
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_name 和 create_time,反序列化时也会按这两个键名回填。
继承
如果子类也需要把父类字段一起纳入序列化,可以在宏参数里显式声明父类:
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) | 通过 TypeInfo 从 DataModel 动态反序列化 |
如果你已经拿到了对象,也可以直接调用实例方法:
// 创建序列化选项对象。
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 个最关键的开关:
| 选项 | 默认值 | 作用 |
|---|---|---|
nullable | JsonNullable.Required | 控制非可空字段缺失或为 null 时是否报错 |
numberHandling | JsonNumberHandling.Strict | 控制数字能否从字符串读取,以及写出时是否转成字符串 |
defaultIgnoreCondition | JsonIgnoreCondition.Never | 控制序列化时是否忽略 None 字段 |
dateFormatString | None | 自定义 DateTime 的读写格式 |
nullable
默认是 JsonNullable.Required。
这意味着:
- 非可空字段缺失,会抛
DataModelException - 非可空字段值为
null,也会抛DataModelException - 可空字段
?T即使缺失或为null,也会保留为None
如果改成 JsonNullable.Disabled,非可空字段在缺失或为 null 时不会报错,而是保留字段声明时的默认值。
// 调整 nullable 策略,让缺失的非可空字段回退到默认值。
let options = JsonSerializerOptions()
options.nullable = JsonNullable.Disabled
let json = #"{"name": "alice"}"#
// 用自定义选项执行反序列化。
let student = JsonSerializer.deserializeObject<Student>(json, options)这里如果 Student.id 是非可空字段,那么它会保留默认值 0,而不是抛异常。
numberHandling
这个选项有两个常用标记:
JsonNumberHandling.AllowReadingFromStringJsonNumberHandling.WriteAsString
可以单独使用,也可以用 | 组合。
// 允许数字从字符串读取,并在写出时转成字符串。
let options = JsonSerializerOptions()
options.numberHandling =
JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString按当前实现:
AllowReadingFromString允许把"42"读成整数,把"3.14"读成浮点数- 对非可空数字字段,空字符串在严格模式下会报错,在
Disabled模式下会回退到默认值 WriteAsString会把整数、浮点和Decimal写成 JSON 字符串
defaultIgnoreCondition
当它被设为 JsonIgnoreCondition.WhenWritingNull 时,值为 None 的可空字段不会写入输出 JSON。
// 序列化时忽略值为 None 的可空字段。
let options = JsonSerializerOptions()
options.defaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull这个开关只影响写出,不影响读取。
dateFormatString
如果字段里有 DateTime,可以用它指定格式:
import std.time.*
import soulsoft_serialization.*
// 设置 DateTime 的统一格式。
let options = JsonSerializerOptions()
options.dateFormatString = "yyyy-MM-dd HH:mm:ss"设置后:
- 序列化会按这个格式输出字符串
- 反序列化也会按同样的格式解析
自定义转换器
如果某个类型的默认行为不符合你的要求,可以继承 JsonConverter<T>。
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 参与的序列化和反序列化都会优先使用这个转换器。
这里有两个行为值得记住:
- 自定义转换器优先于内置转换器
- 同一个类型注册多个转换器时,后注册的优先级更高
支持的类型
按当前源码和单测,框架内置支持下面这些常见类型:
- 整数:
Int8、Int16、Int32、Int64、UInt8、UInt16、UInt32、UInt64 - 浮点:
Float16、Float32、Float64 - 其他基础类型:
Bool、String - 数值与时间:
BigInt、Decimal、DateTime - 可空类型:
?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 标记的类里:
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:
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