- @JsonPropertyName → @SerializerPropertyName - @JsonIgnore → @SerializerIgnore - @JsonParent → @SerializerParent(宏文件 JsonParentMacro.cj → SerializerParentMacro.cj) - 同步更新:JsonAnnotations、ReflectionCache、宏内部注解判断、 测试、方案文档、serialization_test 独立项目 - 16 用例全绿
546 lines
26 KiB
Markdown
546 lines
26 KiB
Markdown
# 方案:父类字段序列化支持(宏 + 静态导出方法)
|
||
|
||
> 状态:待评审
|
||
> 目标仓库:simapi-serialization
|
||
> 关联问题:Cangjie 反射无法通过父类字段句柄读写子类实例的值,导致继承的父类字段无法序列化/反序列化。
|
||
|
||
---
|
||
|
||
## 1. 问题与调研结论
|
||
|
||
### 1.1 现象
|
||
|
||
```cangjie
|
||
open class BaseUser {
|
||
public var _id: String = ""
|
||
public var _createdAt: String = ""
|
||
}
|
||
|
||
class User <: BaseUser {
|
||
public var _name: String = ""
|
||
}
|
||
|
||
let u = User()
|
||
JsonSerializer.Serialize(u)
|
||
// 当前输出只含 User 自身字段:{ "_name": "alice" }
|
||
// 期望输出包含父类字段:{ "_id": "...", "_createdAt": "...", "_name": "..." }
|
||
```
|
||
|
||
### 1.2 根因(已实测确认,cjc 1.1.3)
|
||
|
||
Cangjie 标准库 `std.reflect` 的读写句柄全部带**声明类严格校验**(`variable_info_cjnative.cj` 硬编码):
|
||
|
||
```cangjie
|
||
// InstanceVariableInfo.getValue / setValue
|
||
func checkDeclaringClassType(instance: Any): Unit {
|
||
let declaringClassTypeInfo = getFieldDeclaringClassType(_declaringTypeInfo)
|
||
let instanceTypeInfo = TypeInfo.of(instance)
|
||
if (declaringClassTypeInfo != instanceTypeInfo) {
|
||
throw IllegalTypeException(
|
||
"The input instance should be \"${declaringClassTypeInfo}\", but now it`s \"${instanceTypeInfo}\"")
|
||
}
|
||
}
|
||
```
|
||
|
||
`declaringClassTypeInfo != TypeInfo.of(instance)` 是**严格相等**,不是子类型判断。实测三类句柄全部被堵死:
|
||
|
||
| 句柄 | 在子类实例上读写父类成员 | 实测结果 |
|
||
|---|---|---|
|
||
| `InstanceVariableInfo`(var 字段) | `baseFields.getValue(child)` | ❌ IllegalTypeException |
|
||
| `InstancePropertyInfo`(prop) | `baseProps.getValue(child)` | ❌ IllegalTypeException |
|
||
| `InstanceFunctionInfo`(方法) | `baseFunc.apply(child, args)` | ❌ IllegalTypeException |
|
||
|
||
### 1.3 两个被否掉的路线(均已实测)
|
||
|
||
**路线 A:宏把父类字段「复制」到子类声明里**
|
||
|
||
不可行,Cangjie 编译器**禁止字段遮蔽**:
|
||
|
||
```
|
||
error: the variable '_id' must not shadow a member variable of the supertype
|
||
```
|
||
|
||
即使宏能在子类里生成同名同类型字段,编译器直接拒绝编译。
|
||
|
||
**路线 B:宏在编译期查询父类的字段列表,自动生成代码**
|
||
|
||
不可行,Cangjie 宏是**纯语法层**操作:
|
||
|
||
- 宏的输入只有被标注声明自身的 Tokens(如 `class User <: BaseUser {...}` 这段代码);
|
||
- 宏没有编译期类型查询 API(无类似 Rust proc_macro 的 TypeInfo 上下文);
|
||
- `std.ast` 的 `Position` 只含 `fileID/line/column`,无源码读取 API;
|
||
- 因此宏**看不到父类声明里的字段**(父类可能在别的文件/包)。
|
||
|
||
### 1.4 唯一的可行通路(已实测验证)
|
||
|
||
**静态方法反射调用没有「实例声明类检查」**,且**子类实例可直接作为父类类型参数传入**:
|
||
|
||
```cangjie
|
||
// 父类上定义一个静态方法,参数类型是父类
|
||
public static func exportJsonFields(instance: BaseUser): HashMap<String, Any> {
|
||
var m = HashMap<String, Any>()
|
||
m["_id"] = instance._id // 静态类型访问,编译期就知道字段存在
|
||
m
|
||
}
|
||
|
||
// 反射调用(核心验证通过):
|
||
let f = TypeInfo.of<BaseUser>().getStaticFunction("exportJsonFields", [TypeInfo.of<BaseUser>()])
|
||
let args: Array<Any> = [child] // Child 实例作为 BaseUser 参数 ✓
|
||
let result = f.apply(TypeInfo.of<BaseUser>(), args) // ✓ 调用成功
|
||
```
|
||
|
||
原理:`exportJsonFields` 内部用**静态类型** `BaseUser` 访问 `_id`,走的是普通字段访问(非反射),完全合法;外部通过静态方法反射调用,`checkStaticDeclaringType` 只校验声明的类(用 `thisType` 参数),不校验实例的运行时类型。
|
||
|
||
### 1.5 参考实现:soulsoft_serialization 是怎么做的(已读源码)
|
||
|
||
soulsoft 走的路线与本方案不同——**它完全不用反射读字段**,而是宏给**每个类**生成序列化方法,运行时是普通方法调用链:
|
||
|
||
```cangjie
|
||
@Serialization[superType: BaseUser] // 子类显式告诉宏:我的父类是 BaseUser
|
||
public class User <: BaseUser { ... }
|
||
```
|
||
|
||
宏在编译期(AST 层)为类生成 4 个方法 + 实现 `ISerialization<T>` 接口:
|
||
|
||
| 方法 | 可见性 | 作用 |
|
||
|---|---|---|
|
||
| `serializeObject(options): DataModel` | public | 序列化入口,构造 DataModelStruct 后调追加方法 |
|
||
| `serializeObject(dms, options): Unit` | protected | 追加本类字段(`dms.add(Field("_id", this._id.serializeObject(options)))`,**编译期静态访问**) |
|
||
| `deserializeObject(dm, options): T` | public static | 反序列化入口,构造实例后调追加方法 |
|
||
| `deserializeObject(dms, data, options): Unit` | protected static | 回填本类字段(`data._id = String.deserializeObject(...)`,**编译期静态赋值**) |
|
||
|
||
**继承串联靠编译期方法调用,不是反射**:
|
||
|
||
```cangjie
|
||
// 子类生成的方法,方法体第一行:
|
||
super.serializeObject(dms, options) // 序列化:父类字段由父类方法追加
|
||
BaseUser.deserializeObject(dms, data, options) // 反序列化:先回填父类字段
|
||
```
|
||
|
||
关键点:
|
||
|
||
- 父类字段访问是宏生成的 `this._id` / `data._id = ...`,**静态类型访问**,天然绕开反射的声明类检查(与本方案 §2.2 同理);
|
||
- 运行时 `data.serializeObject(options)` 是接口虚方法调用、`T.deserializeObject(...)` 是泛型静态调用,全部编译期绑定,**零反射、零类型转换**;
|
||
- 宏怎么知道父类是谁?**用户显式传 `superType` 参数**——印证了「宏无法查询类型信息」的限制,soulsoft 用「让用户声明」解决;
|
||
- 代价:**每个类都要标宏**,且 API 带泛型约束 `where T <: ISerializable`——不标宏的类编译期就拒绝,「任意类免标注序列化」能力不存在。
|
||
|
||
**与本方案的关系**:soulsoft 的「宏生成静态类型字段代码」思路与本方案一致,可以借鉴;但它的「每个类标宏 + 泛型约束」形态会破坏 simapi_serialization「任意类免标注、`Serialize(obj)`/`Deserialize<T>(json)`」的 API 契约。因此本方案**只借其「宏生成字段代码」的技巧**,保留自己的反射入口(详见 §8 路线对比)。
|
||
|
||
---
|
||
|
||
## 2. 方案总览
|
||
|
||
> 本方案 = **路线 A**(推荐):保留 simapi_serialization 现有的反射入口 API,
|
||
> 仅对「想暴露父类字段」的父类用宏生成静态导出/导入方法,库沿继承链调用。
|
||
> 路线 B(soulsoft 全宏风格)见 §8 对比,作为备选。
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 宏 @SerializerParent(标注在父类上) │
|
||
│ → 编译期解析父类声明的 var 字段 │
|
||
│ → 自动生成两个静态方法(静态类型访问,无反射检查): │
|
||
│ exportJsonFields(instance: Parent) │
|
||
│ importJsonFields(instance: Parent, jsonObj) │
|
||
└─────────────────────────────────────────────────────────┘
|
||
↓ 运行时
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 序列化库 JsonWriter / JsonReader │
|
||
│ → 遍历 superClass 链(Child → Parent → ... → Object) │
|
||
│ → 每个父类若存在约定静态方法则调用并合并字段 │
|
||
│ → 无宏的类行为不变(向后兼容) │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.1 为什么宏标注在「父类」而不是「子类」
|
||
|
||
宏只能看到自己标注的那份声明的 Tokens。父类自己知道自己有哪些字段,所以宏标注在父类上,才能解析出字段清单并生成代码。子类不需要任何改动,运行时靠继承链自动找到父类的导出方法。
|
||
|
||
### 2.2 为什么能绕过反射限制
|
||
|
||
- 导出方法内部:`instance._id` 是静态类型字段访问(编译期解析),不是反射,无任何运行时检查;
|
||
- 导出方法外部:通过**静态方法反射**调用,静态方法没有「实例声明类」校验,只校验声明类(用 `thisType` 参数),子类实例天然是父类类型的合法值。
|
||
|
||
### 2.3 为什么不用 soulsoft 的「每个类标宏」形态
|
||
|
||
soulsoft 的 API 带泛型约束 `where T <: ISerializable`,不标宏的类在编译期就被拒绝——这要求**所有**序列化类都标注宏,与 simapi_serialization「任意类免标注、`Serialize(obj)`/`Deserialize<T>(json)`」的契约冲突。本方案把「宏生成字段代码」与「反射入口」分层:宏只在父类上补「父类字段」这一块,其余照旧反射,二者互不干扰。
|
||
|
||
---
|
||
|
||
## 3. 宏设计
|
||
|
||
### 3.1 宏定义
|
||
|
||
```cangjie
|
||
// 文件:src/json/SerializerParentMacro.cj
|
||
// 包:simapi_serialization.json(宏包需单独编译,见 3.3)
|
||
|
||
macro package simapi_serialization.json
|
||
|
||
import std.ast.*
|
||
import std.collection.*
|
||
|
||
/// 标注在父类上:自动生成父类字段的 JSON 导出/导入静态方法
|
||
public macro SerializerParent(input: Tokens): Tokens {
|
||
// 1. 解析输入的 ClassDecl(非类声明 → 抛 MacroException)
|
||
// 2. 校验 open:非 open class → 抛 MacroException(见 3.1.1)
|
||
// 3. 收集 public var 字段(跳过 @SerializerIgnore)
|
||
// 4. 生成 exportJsonFields / importJsonFields 两个静态方法
|
||
// 5. 拼回原类声明 + 新方法,返回 Tokens
|
||
}
|
||
```
|
||
|
||
#### 3.1.1 open 处理(两种模式,默认自动补 open)
|
||
|
||
`@SerializerParent` 的语义是「暴露**父类**字段给子类继承链」——标注的类**必须可被继承**。Cangjie 类默认不可继承(非 open 类被继承会编译报错 `super class is not inheritable`),所以宏展开时处理 `open`:
|
||
|
||
```cangjie
|
||
/// 遍历类声明的修饰符,检查是否含 open(参考 soulsoft Extensions.cj isOpen)
|
||
func isOpenClass(decl: ClassDecl): Bool {
|
||
for (m in decl.modifiers) {
|
||
if (m.keyword.kind == TokenKind.OPEN) {
|
||
return true
|
||
}
|
||
}
|
||
false
|
||
}
|
||
```
|
||
|
||
**模式一(推荐):自动补 open**
|
||
|
||
```cangjie
|
||
// 宏展开入口内:
|
||
if (!isOpenClass(decl)) {
|
||
decl.modifiers.add(Modifier(Token(TokenKind.OPEN))) // 自动加 open
|
||
}
|
||
```
|
||
|
||
- **已实测可行**:宏给 `class Base` 自动加 `open` 后,子类 `class Child <: Base` 编译通过、正常运行(`macro_open_probe` 验证);
|
||
- 宏展开的 `open` 在语义检查**之前**生效,所以后续继承检查能看到;
|
||
- 用户少写一个关键字,标注 `@SerializerParent` 本身就表达了「我要被继承」的意图。
|
||
|
||
**模式二:严格校验(不自动补,非 open 报错)**
|
||
|
||
```cangjie
|
||
if (!isOpenClass(decl)) {
|
||
throw ASTException("@SerializerParent 只能标注在 open class 上,"
|
||
+ "因为非 open 类不能被继承,不存在子类场景。")
|
||
}
|
||
```
|
||
|
||
- 抛 `ASTException`(宏内实际可用异常类,soulsoft 同款)→ 编译期直接报错,错误定位到标注处(`main.cj:5:1` 形式);
|
||
- 顺带校验:标注在 **struct / interface / enum / func** 等非 class 声明上也抛错(`parseDecl` 结果 `as ClassDecl` 失败即报,已实测)。
|
||
|
||
**取舍**:自动补 open 更省事但「隐式改变类语义」;严格校验更显式但要求用户记得写 `open`。默认建议模式一(自动补),可在宏属性中提供开关,如 `@SerializerParent[requireOpen: true]` 切到模式二。
|
||
|
||
- 参考先例:soulsoft `Extensions.cj:75-82` 遍历 `modifiers` 判 `TokenKind.OPEN`;`SerializationMacro.cj:27` 用 `throw ASTException(...)` 做宏内校验;`cd.modifiers.add(Modifier(Token(TokenKind.OPEN)))` 与 soulsoft `funcDecl.modifiers.add(Modifier(Token(TokenKind.PUBLIC)))` 同款操作。
|
||
|
||
### 3.2 生成的目标代码
|
||
|
||
对如下父类:
|
||
|
||
```cangjie
|
||
@SerializerParent
|
||
open class BaseUser {
|
||
public var _id: String = ""
|
||
public var _createdAt: String = ""
|
||
@SerializerIgnore
|
||
public var _temp: String = ""
|
||
}
|
||
```
|
||
|
||
宏展开后等价于:
|
||
|
||
```cangjie
|
||
open class BaseUser {
|
||
public var _id: String = ""
|
||
public var _createdAt: String = ""
|
||
@SerializerIgnore
|
||
public var _temp: String = ""
|
||
|
||
/// 导出:字段名 → 值(静态类型访问)
|
||
public static func exportJsonFields(instance: BaseUser): HashMap<String, Any> {
|
||
var m = HashMap<String, Any>()
|
||
m["_id"] = instance._id
|
||
m["_createdAt"] = instance._createdAt
|
||
// _temp 被 @SerializerIgnore 跳过
|
||
m
|
||
}
|
||
|
||
/// 导入:按 JSON 键回填字段(静态类型赋值)
|
||
public static func importJsonFields(instance: BaseUser, json: HashMap<String, Any>): Unit {
|
||
for ((k, v) in json) {
|
||
match (k) {
|
||
case "_id" =>
|
||
if (let s: String <- v) { instance._id = s }
|
||
case "_createdAt" =>
|
||
if (let s: String <- v) { instance._createdAt = s }
|
||
case _ => () // 未知键忽略
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.3 多层继承(已实测验证)
|
||
|
||
库沿 `superClass` 链**逐层**调用每层的 `exportJsonFields`(见 §4),因此任意层数的继承都支持:
|
||
|
||
```
|
||
GrandParent(标宏)→ Parent(标宏)→ Child(不标宏)
|
||
│ │ │
|
||
└─ exportJsonFields └─ exportJsonFields └─ 主反射流程
|
||
导出 _gp 导出 _p 导出 _c
|
||
```
|
||
|
||
**实测结果(cjc 1.1.3,inherit_3level 探针)**:
|
||
|
||
| 场景 | 结果 |
|
||
|---|---|
|
||
| 三层都标宏 | `_gp` + `_p` 均正确导出(`size=2`),子类自身 `_c` 由主反射流程处理 |
|
||
| 中间层不标宏 | 跳过该层继续向上,`_gp` 仍导出(`size=1`),`_p` 不导出 |
|
||
|
||
规则:
|
||
- **每层各导各的**:宏只解析「本类声明」里的字段,父类字段由上层自己的宏负责——不需要在子类方法里递归调用父类方法(这点与 soulsoft 的 `super.serializeObject()` 链不同,本方案靠库侧遍历,宏生成更简单);
|
||
- **中间层无宏自动跳过**:`getStaticFunction` 抛异常即视为无宏,静默继续向祖父层查找;
|
||
- **注意**:若中间层(如 Parent)没标宏,它的字段 `_p` 不会被导出——要导出哪层的字段,哪层就标宏,语义清晰。
|
||
|
||
### 3.4 宏包的编译方式
|
||
|
||
Cangjie 宏需要两步编译(`cjc --compile-macro` 先编宏,再编使用宏的代码)。cjpm 对宏的支持:
|
||
|
||
- 方案一:宏定义放在独立包(如 `simapi_serialization.macros`),`cjpm` 中通过依赖引入,宏包内 `cjpm.toml` 声明为宏包;
|
||
- 方案二:宏定义与库同包,使用 `--compile-macro` 先行编译——需要确认 cjpm 是否自动处理。
|
||
|
||
> ⚠️ 待验证项:cjpm 对同包宏的编译顺序支持(见 §7 风险)。
|
||
|
||
### 3.5 宏需要处理的细节
|
||
|
||
| 细节 | 处理 |
|
||
|---|---|
|
||
| 字段类型 | 用 `TypeInfo.get(字段类型名)` 在运行时做类型转换(import 侧) |
|
||
| `@SerializerPropertyName` | export 键用注解名;import 匹配也用注解名 |
|
||
| `@SerializerIgnore` | 直接跳过,不生成导出/导入语句 |
|
||
| `let`(不可变)字段 | 只导出,不导入(导入侧跳过) |
|
||
| 无字段 | 生成空方法(返回空 map / 空操作),保证约定方法一定存在 |
|
||
| 命名冲突 | 若用户已手写同名静态方法,宏应报错或跳过(设计取舍:报错更安全) |
|
||
| 泛型父类 | v1 不支持,文档注明(反射对泛型类型参数处理复杂) |
|
||
| 多层继承 | 每个父类各自标注宏,序列化库逐层调用 |
|
||
|
||
---
|
||
|
||
## 4. 序列化库改动
|
||
|
||
### 4.1 JsonWriter(序列化侧)
|
||
|
||
`writeObject` 中,反射写完子类自身字段后,**沿 superClass 链向上**逐个检查约定方法:
|
||
|
||
```cangjie
|
||
private static func writeObject(value: Any, ct: ClassTypeInfo, options: JsonOption, depth: Int64): JsonValue {
|
||
let obj = JsonObject()
|
||
// 1) 子类自身字段(现状不变)
|
||
let fields = ReflectionCache.getFields(ct, options)
|
||
for (f in fields) {
|
||
if (f.ignore) { continue }
|
||
let raw = f.variable.getOrThrow().getValue(value)
|
||
obj.put(f.jsonName, writeValue(raw, options, depth + 1))
|
||
}
|
||
// 2) 父类字段(新增):沿继承链调用约定静态方法
|
||
var parent = ct.superClass
|
||
while (let Some(p) <- parent) {
|
||
let parentFields = try {
|
||
let f = p.getStaticFunction("exportJsonFields", [p])
|
||
let args: Array<Any> = [value] // 子类实例作父类参数
|
||
(f.apply(p, args) as HashMap<String, Any>) ?? HashMap<String, Any>()
|
||
} catch (_: Exception) {
|
||
HashMap<String, Any>() // 无宏的父类 → 空,继续上层
|
||
}
|
||
for ((k, v) in parentFields) {
|
||
obj.put(k, writeValue(v, options, depth + 1))
|
||
}
|
||
parent = p.superClass
|
||
}
|
||
obj
|
||
}
|
||
```
|
||
|
||
要点:
|
||
- 静态方法返回的是**已序列化的字段值**(Any),直接 `writeValue` 二次处理即可(嵌套对象/集合/枚举都会正确递归);
|
||
- 异常即「无此方法」,静默继续向上,**无宏的类完全不受影响**;
|
||
- 需加缓存:父类→是否含约定方法,避免每次反射查找(性能)。
|
||
|
||
### 4.2 JsonReader(反序列化侧)
|
||
|
||
`readObject` 中,构造实例并写完子类字段后,同样沿 superClass 链调用约定方法回填:
|
||
|
||
```cangjie
|
||
private static func readObject(value: JsonValue, ct: ClassTypeInfo, options: JsonOption, depth: Int64): Any {
|
||
let instance = ct.construct([])
|
||
// 1) 子类自身字段(现状不变)
|
||
let fields = ReflectionCache.getFields(ct, options)
|
||
let jobj = value.asObject()
|
||
for (f in fields) {
|
||
if (f.ignore || !f.mutable) { continue }
|
||
match (jobj.get(f.jsonName)) {
|
||
case Some(jv) =>
|
||
if (jv is JsonNull) { continue }
|
||
let fieldVal = readValue(jv, f.typeInfo.getOrThrow(), options, depth + 1)
|
||
f.variable.getOrThrow().setValue(instance, fieldVal)
|
||
case None => ()
|
||
}
|
||
}
|
||
// 2) 父类字段(新增):收集 JSON 中父类能识别的键
|
||
var parent = ct.superClass
|
||
while (let Some(p) <- parent) {
|
||
let importFn = try {
|
||
p.getStaticFunction("importJsonFields", [p, TypeInfo.of<HashMap<String, Any>>()])
|
||
} catch (_: Exception) {
|
||
// 无宏的父类 → 跳过
|
||
parent = p.superClass
|
||
continue
|
||
}
|
||
// 只把父类认识的键传给导入方法(避免把子类字段塞给父类)
|
||
let parentKeys = exportKeysOf(p) // 复用 exportJsonFields 的键名集合(缓存)
|
||
var pjson = HashMap<String, Any>()
|
||
for (k in parentKeys) {
|
||
match (jobj.get(k)) {
|
||
case Some(jv) => pjson[k] = jsonToAny(jv)
|
||
case None => ()
|
||
}
|
||
}
|
||
importFn.apply(p, [instance, pjson])
|
||
parent = p.superClass
|
||
}
|
||
instance
|
||
}
|
||
```
|
||
|
||
要点:
|
||
- 导入方法只回填它认识的键,未知键忽略(宏已生成 `case _ => ()`);
|
||
- 传给导入方法的 JSON 值用 `jsonToAny` 还原为通用 Any,由宏内部做 `as` 转换;
|
||
- null 值:宏侧 `if (let s: String <- v)` 天然跳过 null,保持「None/默认值」语义。
|
||
|
||
### 4.3 缓存
|
||
|
||
新增 `ReflectionCache` 缓存:
|
||
|
||
```cangjie
|
||
// 类型 → 是否有 exportJsonFields/importJsonFields 约定方法(Bool)
|
||
private static let _parentExportCache = HashMap<String, Bool>()
|
||
private static let _parentImportCache = HashMap<String, Bool>()
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 使用示例
|
||
|
||
```cangjie
|
||
import simapi_serialization.*
|
||
import simapi_serialization.json.SerializerParent // 引入宏
|
||
|
||
// 父类标注宏
|
||
@SerializerParent
|
||
open class BaseUser {
|
||
public var _id: String = ""
|
||
public var _createdAt: String = ""
|
||
}
|
||
|
||
// 子类零改动
|
||
class User <: BaseUser {
|
||
public var _name: String = ""
|
||
public var _age: Int64 = 0
|
||
}
|
||
|
||
main() {
|
||
let u = User()
|
||
u._id = "u-1"
|
||
u._createdAt = "2025-01-01"
|
||
u._name = "alice"
|
||
u._age = 30
|
||
|
||
let json = JsonSerializer.Serialize(u)
|
||
// 输出(字段顺序:子类自身字段 + 父类字段):
|
||
// { "_name": "alice", "_age": 30, "_id": "u-1", "_createdAt": "2025-01-01" }
|
||
|
||
let u2 = JsonSerializer.Deserialize<User>(json)
|
||
// u2._id == "u-1" ✓ u2._name == "alice" ✓
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 测试计划
|
||
|
||
在 `src/json/` 下新增测试(复用现有 `JsonSerializer_test.cj` 的测试基建):
|
||
|
||
| 用例 | 断言 |
|
||
|---|---|
|
||
| 父类字段序列化 | 输出包含 `_id`/`_createdAt`,且 `@SerializerIgnore` 父类字段不输出 |
|
||
| 父类字段反序列化 | `Deserialize<User>` 后父类字段值正确 |
|
||
| 多层继承 | GrandParent → Parent → Child 三层字段全部输出 |
|
||
| 中间层无宏 | 三层链中 Parent 不标宏:`_gp` 仍导出,`_p` 不导出(逐层跳过) |
|
||
| 无宏的类 | 行为与现状完全一致(回归) |
|
||
| 子类字段与父类字段重名 | 子类优先(输出一份,值取子类的) |
|
||
| 父类嵌套对象字段 | 父类字段值是对象时正确递归 |
|
||
| 父类字段 null | 导入时保持默认值 |
|
||
| 与 @SerializerPropertyName 组合 | 父类字段用注解名输出/匹配 |
|
||
| 顶层序列化父类类型实例 | `Serialize(BaseUser 实例)` 直接可用 |
|
||
| **宏自动补 open** | `@SerializerParent class Base`(无 open)展开后子类可继承(编译通过) |
|
||
| **宏严格模式报错** | `@SerializerParent[requireOpen: true]` 标注非 open class 编译失败 |
|
||
| **宏非 class 标注报错** | 对 struct/interface/enum 标注 `@SerializerParent` 编译失败 |
|
||
|
||
---
|
||
|
||
## 7. 风险与待验证项
|
||
|
||
| # | 风险 | 说明/对策 |
|
||
|---|---|---|
|
||
| 1 | **cjpm 对宏包的支持** | Cangjie 宏需 `cjc --compile-macro` 先行编译;需验证 cjpm 依赖系统能否自动处理宏包(若不能,改独立宏包目录 + 手动编译脚本,或文档说明编译顺序) |
|
||
| 2 | 宏解析字段的复杂度 | 需要处理 `public var`/`public let`/注解/默认值表达式;用 `parseDecl` 解析后遍历 `ClassDecl.members` 中的 `VarDecl` |
|
||
| 3 | `as HashMap<String, Any>` 的类型转换 | 静态方法反射 `apply` 返回 `Any`,需 `??` 兜底;若反射返回类型严格匹配失败,可在宏生成时返回 `Array<(String, Any)>` 或专用 DTO 规避 |
|
||
| 4 | 父类字段顺序 | 继承链遍历顺序(父类在前还是后)影响 JSON 键顺序;JSON 对象键无序,不构成功能问题 |
|
||
| 5 | 与现有「父类字段不序列化」测试冲突 | 现有测试 `objectSerialize` 断言 `_id`/`_createdAt` 不输出——**该断言需在宏方案落地后反转/条件化**(无宏时仍不输出,有宏时输出) |
|
||
|
||
---
|
||
|
||
## 8. 路线对比与备选方案
|
||
|
||
### 8.1 两条可行路线的取舍
|
||
|
||
| | 路线 A:入口不变 + 父类宏(本方案) | 路线 B:soulsoft 全宏风格 |
|
||
|---|---|---|
|
||
| 宏标在哪 | **仅父类**标 `@SerializerParent` | **每个类**标 `@Serialization[superType: 父类]` |
|
||
| 继承串联 | 库运行时反射 `superClass` 链 + 静态方法调用 | 编译期 `super.serializeObject(...)` / `Parent.deserializeObject(...)` 调用链 |
|
||
| `Serialize(obj)` / `Deserialize<T>(json)` | ✅ **完全不变**(无泛型约束,任意类) | 需保留反射回退,否则无宏类编译期拒绝 |
|
||
| 无宏的类 | ✅ 照旧全反射(向后兼容) | 无法序列化(除非入口加反射回退) |
|
||
| 调用方改动 | 无 | 每个类要加标注 + `superType` |
|
||
| 运行时开销 | 一次静态方法反射查找(可缓存) | 零反射,纯方法调用 |
|
||
| 类型安全 | 导出方法内静态访问(安全),返回值 `as` 转换(需兜底) | 全程编译期强类型 |
|
||
| 宏实现复杂度 | 中(解析字段生成 2 个静态方法) | 高(生成 4 方法 + 接口实现 + 构造器 + prop) |
|
||
| 字段约定 | 无(所有 public var) | 必须 `_` 前缀 + var + 类型注解(`isAllowedMemberVarDecl`) |
|
||
|
||
**结论**:路线 B 的运行时性能更优,但要求每个类标宏、约束字段命名、改变 API 形态——与 simapi_serialization「任意类免标注」的核心价值冲突。**推荐路线 A**:只借 soulsoft「宏生成静态类型字段代码」的技巧,入口 API、调用方、无宏类行为全部保持现状。
|
||
|
||
### 8.2 备选方案总表
|
||
|
||
| 方案 | 可行性 | 改动量 | 说明 |
|
||
|---|---|---|---|
|
||
| **路线 A:宏标注父类 + 静态导出方法(推荐)** | ✅ 已验证 | 中 | 自动、向后兼容、无反射限制、API 不变 |
|
||
| 路线 B:soulsoft 全宏风格(每类标宏 + superType) | ✅ 可参考 | 高 | 零反射,但破坏免标注 API、约束字段命名 |
|
||
| 序列化库运行时遍历继承链 + 反射读父类字段 | ❌ | — | `getValue` 声明类严格校验,直接抛异常 |
|
||
| 宏把父类字段复制到子类 | ❌ | — | 编译器禁止字段遮蔽 |
|
||
| 用户在子类手写父类字段(复制粘贴) | ✅ | 小但人工 | 破坏继承语义、易漏改;仅作为临时 workaround |
|
||
| 序列化库要求父类字段改用 prop + 方法反射 | ❌ | — | 方法反射同样有声明类校验(已实测) |
|
||
|
||
---
|
||
|
||
## 9. 实施步骤(评审通过后)
|
||
|
||
1. 新建宏文件 `src/json/SerializerParentMacro.cj`(或独立宏包),实现 `SerializerParent` 宏;
|
||
2. 验证 cjpm 对宏包的编译流程(风险 #1),必要时调整项目结构;
|
||
3. 改 `JsonWriter.writeObject`:superClass 链调用 `exportJsonFields`;
|
||
4. 改 `JsonReader.readObject`:superClass 链调用 `importJsonFields`;
|
||
5. `ReflectionCache` 增加约定方法存在性缓存;
|
||
6. 新增 §6 测试用例;调整现有 `objectSerialize` 断言(风险 #5);
|
||
7. `cjpm test` 全绿;
|
||
8. 提交并推送。
|