Files
serialization-cj/docs/父类字段序列化方案.md
T
xrain 990f10f98c refactor: 注解与宏改名——Serializer 前缀统一命名
- @JsonPropertyName → @SerializerPropertyName
- @JsonIgnore → @SerializerIgnore
- @JsonParent → @SerializerParent(宏文件 JsonParentMacro.cj → SerializerParentMacro.cj)
- 同步更新:JsonAnnotations、ReflectionCache、宏内部注解判断、
  测试、方案文档、serialization_test 独立项目
- 16 用例全绿
2026-08-17 13:58:05 +08:00

546 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 方案:父类字段序列化支持(宏 + 静态导出方法)
> 状态:待评审
> 目标仓库: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.3inherit_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. 提交并推送。