Skip to content

序列化

@Serialization 编译期宏自动生成序列化代码——只维护类定义,无需手写几百行重复逻辑。

适用场景

  • 对象与 JSON 之间需要频繁互转
  • 类字段较多(10+),手写序列化代码量大且容易漏字段
  • 字段经常变动,手工同步四处定义(属性、序列化、反序列化、构造器)成本高
  • 需要 Entity ↔ DTO 字段级映射,不经过 JSON 字符串

不适用:

  • 只有 2-3 个字段且几乎不变——手写负担可以接受
  • 序列化逻辑高度定制且每个字段行为差异极大——自动生成反而不灵活

相关依赖

包名作用什么时候需要
soulsoft_serialization提供 JsonSerializerDataModelJsonSerializerOptionsJsonConverter<T> 等核心类型始终需要
soulsoft_serialization.macros提供 @Serialization@DataField 等编译期宏需要自动生成序列化代码时

快速开始

引入 soulsoft_serializationsoulsoft_serialization.macros 后,3 步搞定:

cangjie
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
序列化对象转 JSONJsonSerializer.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() 实际怎么干活。

宏生成的代码

对每个满足条件的字段,宏自动生成:

  • 去掉下划线的公开属性_idid_contentcontent
  • serializeObject()deserializeObject() 方法
  • 一个按字段展开的构造器(如果你手写了同参数构造器,宏不再生成)
cangjie
@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 只处理满足三条规则的字段:

cangjie
@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

cangjie
@Serialization
class ChatMessage {
    private var _id: Int64 = 0

    @DataField["message_role"]
    private var _role: String = ""

    @DataField["create_time"]
    private var _createdAt: String = ""
}

序列化输出 message_rolecreate_time,反序列化也按这两个键回填。

继承:子类用 superType 声明父类,序列化时先处理父类字段再处理子类字段,反序列化同理。多级继承每层都声明 superType 即可。

cangjie
@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() 告诉宏"构造器我来写",宏就不再生成展开构造器。

自定义属性:手写了同名公开属性,宏不再重复生成。适合暴露只读视图:

cangjie
@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 → 对象(指定选项)

基本用法:

cangjie
@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 → ChatSession

serializeObject() 实例方法由宏直接生成在类上,返回 DataModel 中间表示。JsonSerializer 的静态方法封装了 DataModel ↔ JSON 字符串的完整流程。

反序列化时:

  • 多余的未知字段被忽略,不报错
  • 输入必须是 JSON 对象 {};传入数组 [] 或基本值会抛 DataModelException

注意

对象反序列化要求输入是 JSON 对象 {}。传入 [] 会抛 DataModelException——这是输入格式层面的限制,不是字段层面的 null。

类型映射(Entity ↔ DTO):不经过 JSON 字符串,两个 @Serialization 类之间通过 DataModel 直接映射:

cangjie
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)
    }
}

用法:先 serializeObjectDataModel,再 deserializeObject<T> → 目标类型。同名属性自动匹配,目标类没有的字段忽略:

cangjie
@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>
原始 JSONJsonValue
自定义类@Serialization 的 class / struct

几点注意:

  • BigInt 默认写成 JSON 字符串;Decimal 默认写成数字,开启 WriteAsString 后写成字符串
  • HashMap<K, V> 序列化为 JSON 对象,键转为字符串
  • JsonValue 直接内嵌到最终 JSON,不额外包字符串
  • 集合元素是自定义类型时,元素类也要加 @Serialization

选项控制

JsonSerializerOptions 有 4 个关键配置:

选项类型默认值作用
nullableJsonNullableRequired非可空字段缺失/null 时报错还是保留默认值
numberHandlingJsonNumberHandlingStrict数字能否从字符串读、是否写为字符串
dateFormatString?StringNoneDateTime 的读写格式
defaultIgnoreConditionJsonIgnoreConditionNeverNone 的可空字段是否写入 JSON

nullable:

行为
JsonNullable.Required(默认)非可空字段缺失/null → 抛 DataModelException,异常消息带字段名
JsonNullable.Disabled非可空字段缺失/null → 保留字段默认值
cangjie
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 字符串
cangjie
let options = JsonSerializerOptions()
options.numberHandling =
    JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString

dateFormatString:

cangjie
let options = JsonSerializerOptions()
options.dateFormatString = "yyyy-MM-dd HH:mm:ss"

设置后序列化和反序列化都按这个格式处理 DateTime。不设则用默认 ISO 格式。

defaultIgnoreCondition:

行为
JsonIgnoreCondition.Never(默认)所有字段都写入 JSON,包括 None
JsonIgnoreCondition.WhenWritingNullNone 的可空字段不写入 JSON

只影响写出,不影响读取。

自定义转换器

继承 JsonConverter<T>,重写 readwrite

cangjie
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

cangjie
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.defaultIgnoreConditionWhenWritingNull 且字段是可空 ?T、值为 None 时会跳过。

反序列化后值不对

检查是否被自定义转换器修改了值,或者 options.numberHandling 的标记影响了读写。

DateTime 格式不对

默认是 ISO 格式。需要自定义时设 options.dateFormatString

相关专题