序列化
用
@Serialization编译期宏自动生成序列化代码——只维护类定义,无需手写几百行重复逻辑。
适用场景
- 对象与 JSON 之间需要频繁互转
- 类字段较多(10+),手写序列化代码量大且容易漏字段
- 字段经常变动,手工同步四处定义(属性、序列化、反序列化、构造器)成本高
- 需要 Entity ↔ DTO 字段级映射,不经过 JSON 字符串
不适用:
- 只有 2-3 个字段且几乎不变——手写负担可以接受
- 序列化逻辑高度定制且每个字段行为差异极大——自动生成反而不灵活
相关依赖
| 包名 | 作用 | 什么时候需要 |
|---|---|---|
soulsoft_serialization | 提供 JsonSerializer、DataModel、JsonSerializerOptions、JsonConverter<T> 等核心类型 | 始终需要 |
soulsoft_serialization.macros | 提供 @Serialization、@DataField 等编译期宏 | 需要自动生成序列化代码时 |
快速开始
引入 soulsoft_serialization 和 soulsoft_serialization.macros 后,3 步搞定:
import soulsoft_serialization.*
import soulsoft_serialization.macros.*
// 1. 定义类 — 加 @Serialization,字段写 _field: Type = default
@Serialization
class ChatPromptTemplate {
private var _name: String = ""
private var _temperature: Float64 = 0.7
}
main(): Int64 {
// 2. 序列化 — 对象 → JSON 字符串
let prompt = ChatPromptTemplate(name: "support-agent", temperature: 0.7)
let json = JsonSerializer.serializeObject(prompt)
println(json)
// 3. 反序列化 — JSON 字符串 → 对象
let restored = JsonSerializer.deserializeObject<ChatPromptTemplate>(json)
println(restored.name)
return 0
}运行输出:
{"name":"support-agent","temperature":0.7}
support-agent整个流程只有三个角色:
| 步骤 | 做什么 | 用的类型 |
|---|---|---|
| 定义 | 标记类,声明字段 | @Serialization |
| 序列化 | 对象转 JSON | JsonSerializer.serializeObject() |
| 反序列化 | JSON 转对象 | JsonSerializer.deserializeObject<T>() |
下面逐一展开。
核心概念
编译期与运行时两阶段
序列化分两个阶段:编译期宏生成代码,运行时执行转换。
编译期 运行时
═══════ ═══════
@Serialization
class ChatSession { JsonSerializer.serializeObject(session)
private var _id: Int64 │
private var _title: String ▼
} ┌──────────────────────┐
│ │ 遍历字段 │
│ 宏展开 │ _id → DataModelInt │
▼ │ _title → DataModelStr│
ChatSession 自动实现: │ 每个字段调对应的 │
ISerialization<ChatSession> │ serializeObject() │
│ └──────┬───────────────┘
├── serializeObject() │
├── deserializeObject() ▼
├── 公开属性 id / title JsonSerializerOptions
└── 全字段构造器 │
├── converters[] ← 自定义转换器
├── nullable ← null 处理策略
├── numberHandling ← 数字读写策略
└── dateFormatString ← 日期格式@Serialization 宏是为编译器写的蓝图——它读字段声明,生成 ISerialization<T> 的完整实现。JsonSerializerOptions 是给运行时看的配置——它决定宏生成的 serializeObject() / deserializeObject() 实际怎么干活。
宏生成的代码
对每个满足条件的字段,宏自动生成:
- 去掉下划线的公开属性(
_id→id,_content→content) serializeObject()和deserializeObject()方法- 一个按字段展开的构造器(如果你手写了同参数构造器,宏不再生成)
@Serialization
class ChatPromptParams {
private var _temperature: Float64 = 0.7
private var _maxTokens: Int64 = 1024
}
let p = ChatPromptParams(temperature: 0.7, maxTokens: 1024)
p.temperature = 0.9
println(p.maxTokens)宏生成了三样东西:按字段展开的构造器(x: 1.0, y: 2.0)、属性的 getter/setter、以及完全透明的外部使用方式。
DataModel 中间表示
DataModel 是序列化的中间表示——序列化时对象先转为 DataModel,再转为 JSON 字符串;反序列化时 JSON 先解析为 DataModel,再转为对象。选项控制每一步怎么转:数字要不要写成字符串、null 字段要不要跳过、日期用什么格式。
DataModel 的价值不止于此。两个 @Serialization 类之间可以通过 DataModel 直接映射,不经过 JSON 字符串——适合 Entity ↔ DTO 的字段级搬运(见常见用法 → 序列化与反序列化)。
常见用法
定义可序列化类
@Serialization 只处理满足三条规则的字段:
@Serialization
class ChatMessage {
private var _id: Int64 = 0 // ✓ 下划线开头 + var + 显式类型
private var _content: String = "" // ✓
private var _optTokens: ?Int64 = None // ✓ 可空类型也支持
// 下面这些不会参与序列化:
private var id: Int64 = 0 // ✗ 没有下划线
private let _id: Int64 = 0 // ✗ let 不是 var
private var _id = 0 // ✗ 没写类型
}字段名映射:JSON 键名默认是去掉下划线后的字段名。不匹配时用 @DataField:
@Serialization
class ChatMessage {
private var _id: Int64 = 0
@DataField["message_role"]
private var _role: String = ""
@DataField["create_time"]
private var _createdAt: String = ""
}序列化输出 message_role 和 create_time,反序列化也按这两个键回填。
继承:子类用 superType 声明父类,序列化时先处理父类字段再处理子类字段,反序列化同理。多级继承每层都声明 superType 即可。
@Serialization
open class BaseEntity {
private var _id: Int64 = 0
}
@Serialization[superType: BaseEntity]
class Article {
private var _title: String = ""
public init() {}
}这里 Article 序列化时会先处理 BaseEntity 的 _id,再处理自己的 _title。手写 init() 告诉宏"构造器我来写",宏就不再生成展开构造器。
自定义属性:手写了同名公开属性,宏不再重复生成。适合暴露只读视图:
@Serialization
class ChatTranscript {
private var _messages: ArrayList<String> = ArrayList<String>()
public prop messages: ArrayList<String> {
get() { _messages }
}
}定义速查:
| 需求 | 做法 |
|---|---|
| 类参与序列化 | 加 @Serialization |
| JSON 键名 ≠ 字段名 | 字段加 @DataField["json_name"] |
| 子类继承父类字段 | @Serialization[superType: ParentClass] |
| 暴露只读属性 | 手写 public prop,宏不覆盖 |
| 手写构造器 | 写无参 init(),宏不生成展开构造器 |
序列化与反序列化
JsonSerializer 是所有操作的入口。
| 方法 | 用途 |
|---|---|
serializeObject(data) | 对象 → JSON 字符串(默认选项) |
serializeObject(data, options) | 对象 → JSON 字符串(指定选项) |
deserializeObject<T>(json) | JSON 字符串 → 对象(默认选项) |
deserializeObject<T>(json, options) | JSON 字符串 → 对象(指定选项) |
deserializeObject<T>(dm) | DataModel → 对象 |
deserializeObject<T>(dm, options) | DataModel → 对象(指定选项) |
基本用法:
@Serialization
class ChatSession {
private var _id: Int64 = 0
private var _title: String = ""
}
let session = ChatSession(id: 1, title: "support-session")
// 对象 → JSON 字符串
let json = JsonSerializer.serializeObject(session)
println(json) // {"id":1,"title":"support-session"}
// JSON 字符串 → 对象
let restored = JsonSerializer.deserializeObject<ChatSession>(json)
println(restored.id) // 1
println(restored.title) // support-session
// DataModel 中转 — 对象 → DataModel → JSON
let dm = session.serializeObject() // ChatSession → DataModel
let json2 = JsonSerializer.serializeObject(session) // DataModel → JSON 字符串
let obj = JsonSerializer.deserializeObject<ChatSession>(dm) // DataModel → ChatSessionserializeObject() 实例方法由宏直接生成在类上,返回 DataModel 中间表示。JsonSerializer 的静态方法封装了 DataModel ↔ JSON 字符串的完整流程。
反序列化时:
- 多余的未知字段被忽略,不报错
- 输入必须是 JSON 对象
{};传入数组[]或基本值会抛DataModelException
注意
对象反序列化要求输入是 JSON 对象 {}。传入 [] 会抛 DataModelException——这是输入格式层面的限制,不是字段层面的 null。
类型映射(Entity ↔ DTO):不经过 JSON 字符串,两个 @Serialization 类之间通过 DataModel 直接映射:
import soulsoft_serialization.*
class MapperUtilities {
private static let _options = JsonSerializerOptions()
static init() {
_options.nullable = JsonNullable.Disabled
}
public static func map<T>(value: ISerializable) where T <: ISerialization<T> {
let dm = value.serializeObject(_options)
return JsonSerializer.deserializeObject<T>(dm, _options)
}
}用法:先 serializeObject → DataModel,再 deserializeObject<T> → 目标类型。同名属性自动匹配,目标类没有的字段忽略:
@Serialization
class ChatSessionEntity {
private var _id: Int64 = 0
private var _title: String = ""
private var _internalNote: String = "" // DTO 里没有 → 忽略
}
@Serialization
class ChatSessionDto {
private var _id: Int64 = 0
private var _title: String = ""
}
let entity = ChatSessionEntity(id: 1, title: "support-session", internalNote: "vip-user")
let dto = MapperUtilities.map<ChatSessionDto>(entity)
println(dto.title) // support-session关键配置:nullable = JsonNullable.Disabled 确保目标类型缺少的字段不会抛 DataModelException,而是静默跳过保留默认值。
支持的类型速查:
| 类别 | 类型 |
|---|---|
| 整数 | Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64 |
| 浮点 | Float16 Float32 Float64 |
| 基础 | Bool String Rune |
| 数值/时间 | BigInt Decimal DateTime |
| 可空 | ?T / Option<T> |
| 集合 | Array<T> ArrayList<T> HashSet<T> HashMap<K, V> |
| 原始 JSON | JsonValue |
| 自定义类 | 加 @Serialization 的 class / struct |
几点注意:
BigInt默认写成 JSON 字符串;Decimal默认写成数字,开启WriteAsString后写成字符串HashMap<K, V>序列化为 JSON 对象,键转为字符串JsonValue直接内嵌到最终 JSON,不额外包字符串- 集合元素是自定义类型时,元素类也要加
@Serialization
选项控制
JsonSerializerOptions 有 4 个关键配置:
| 选项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
nullable | JsonNullable | Required | 非可空字段缺失/null 时报错还是保留默认值 |
numberHandling | JsonNumberHandling | Strict | 数字能否从字符串读、是否写为字符串 |
dateFormatString | ?String | None | DateTime 的读写格式 |
defaultIgnoreCondition | JsonIgnoreCondition | Never | None 的可空字段是否写入 JSON |
nullable:
| 值 | 行为 |
|---|---|
JsonNullable.Required(默认) | 非可空字段缺失/null → 抛 DataModelException,异常消息带字段名 |
JsonNullable.Disabled | 非可空字段缺失/null → 保留字段默认值 |
let options = JsonSerializerOptions()
options.nullable = JsonNullable.Disabled
let json = #"{"name": "alice"}"# // 缺少 id
let student = JsonSerializer.deserializeObject<Student>(json, options)
println(student.id) // 0(保留默认值)可空字段 ?T 不受此选项影响——缺失或 null 始终保留为 None。
numberHandling:可组合的两个标记(用 | 组合):
| 标记 | 作用 |
|---|---|
JsonNumberHandling.AllowReadingFromString | 允许把 "42" 读成 Int64,"3.14" 读成 Float64 |
JsonNumberHandling.WriteAsString | 整数、浮点和 Decimal 写成 JSON 字符串 |
let options = JsonSerializerOptions()
options.numberHandling =
JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsStringdateFormatString:
let options = JsonSerializerOptions()
options.dateFormatString = "yyyy-MM-dd HH:mm:ss"设置后序列化和反序列化都按这个格式处理 DateTime。不设则用默认 ISO 格式。
defaultIgnoreCondition:
| 值 | 行为 |
|---|---|
JsonIgnoreCondition.Never(默认) | 所有字段都写入 JSON,包括 None |
JsonIgnoreCondition.WhenWritingNull | None 的可空字段不写入 JSON |
只影响写出,不影响读取。
自定义转换器
继承 JsonConverter<T>,重写 read 和 write:
import soulsoft_serialization.*
import stdx.encoding.json.*
class TrimStringConverter <: JsonConverter<String> {
public func read(dm: DataModel, options: JsonSerializerOptions): String {
match (dm) {
case v: DataModelString => v.getValue().trim()
case _ => throw DataModelException("expected String")
}
}
public func write(value: String, options: JsonSerializerOptions): DataModel {
return value.trim().serialize()
}
}注册到 options.converters:
let options = JsonSerializerOptions()
options.converters.add(TrimStringConverter())自定义转换器优先于内置转换器;同一类型注册多个时,后注册的优先级更高。
最佳实践
- 只维护类定义:字段变更只改一处,宏自动更新属性、序列化、反序列化、构造器
- 跨层传输用 DataModel 中转:Entity ↔ DTO 映射跳过 JSON 字符串,节省 CPU 和内存;
nullable = JsonNullable.Disabled让缺失字段静默跳过 - 复用清洗逻辑:多个字段需要同一种清洗(trim、格式归一化)时,写
JsonConverter<T>注册到options.converters,不要在每个 getter/setter 里重复
常见问题
反序列化报错"找不到字段"
检查三项:字段名以下划线开头了吗?是 var 不是 let?写了显式类型?三者缺一不可。
序列化后 JSON 里某个字段消失了
options.defaultIgnoreCondition 是 WhenWritingNull 且字段是可空 ?T、值为 None 时会跳过。
反序列化后值不对
检查是否被自定义转换器修改了值,或者 options.numberHandling 的标记影响了读写。
DateTime 格式不对
默认是 ISO 格式。需要自定义时设 options.dateFormatString。