feat: 注解重命名(@PropertyName/@Ignore) + 新增 @NotRequired 默认必填语义, API 小写化 serialize/deserialize, 宏支持多注解组合, 版本号 1.2.0

This commit is contained in:
2026-08-25 11:59:13 +08:00
parent 1071d18a62
commit cd1b56fb81
12 changed files with 632 additions and 265 deletions
+59 -20
View File
@@ -3,9 +3,10 @@
SimApi 全反射 JSON 序列化库(仓颉版),对齐 .NET `System.Text.Json` 的用法与注解风格。
- **免标注**:任意类(无参构造 + `var` 字段)无需实现接口、无需标注,开箱即用
- **注解**:`@SerializerPropertyName` / `@SerializerIgnore`(对齐 `[JsonPropertyName]` / `[JsonIgnore]`)
- **注解**:`@PropertyName` / `@Ignore` / `@NotRequired`(对齐 `[JsonPropertyName]` / `[JsonIgnore]`,默认必填、`@NotRequired` 允许缺失)
- **默认必填**:反序列化时要求每个 `var` 字段的键必须存在;标注 `@NotRequired` 的字段允许缺失(保持默认值)
- **继承支持**:`@SerializerParent` 宏解决父类字段序列化(Cangjie 反射限制)
- **API 对齐**:`JsonSerializer.Serialize(obj, JsonOption)` / `Deserialize<T>(json, JsonOption)`
- **API 对齐**:`JsonSerializer.serialize(obj, JsonOption)` / `deserialize<T>(json, JsonOption)`
- **字段名**:`_` 前缀与无下划线字段均支持(`_name` 原样输出 `_name`,`name` 输出 `name`)
## 快速开始
@@ -18,14 +19,14 @@ SimApi 全反射 JSON 序列化库(仓颉版),对齐 .NET `System.Text.Jso
```toml
[dependencies]
"simcu::serialization" = "1.0.3"
"simcu::serialization" = "1.2.0"
```
**方式二:Git 仓库**
```toml
[dependencies]
"simcu::serialization" = { git = "https://gitcode.com/simcu/serialization-cj.git", version = "1.0.3" }
"simcu::serialization" = { git = "https://gitcode.com/simcu/serialization-cj.git", version = "1.2.0" }
```
> 本地开发也可用 path 依赖:`"simcu::serialization" = { path = "../simapi-serialization" }`
@@ -51,10 +52,10 @@ let u = User()
u._name = "alice"
u._age = 30
let json = JsonSerializer.Serialize(u)
let json = JsonSerializer.serialize(u)
// { "_name": "alice", "_age": 30, "active": true, "_role": "Member", "opt": null, "_tags": [], "addr": {...} }
let u2 = JsonSerializer.Deserialize<User>(json)
let u2 = JsonSerializer.deserialize<User>(json)
// 全部字段回填
```
@@ -64,14 +65,52 @@ let u2 = JsonSerializer.Deserialize<User>(json)
```cangjie
class User {
@SerializerPropertyName["user_name"]
@PropertyName["user_name"]
public var _username: String = "" // JSON 键用 user_name(对齐 [JsonPropertyName])
@SerializerIgnore
@Ignore
public var _temp: String = "" // 序列化/反序列化忽略(对齐 [JsonIgnore])
@NotRequired
public var _nickname: String = "" // 反序列化时该键允许缺失(默认所有字段必填)
public var _id: String = "" // 默认必填:反序列化时键必须存在,否则抛异常
}
```
> 注解类构造器**必须写 `public const init() {}`**(`@Annotation` 强制 const 构造,`@Ignore`/`@NotRequired` 这类无参注解也不例外);缺省/非 const 都会编译报错。
#### @PropertyName
`字段名 → JSON 键名`,序列化与反序列化双向生效(对齐 `[JsonPropertyName]`)。
```cangjie
@PropertyName["user_name"] public var _username: String = ""
```
#### @Ignore
序列化/反序列化时跳过该字段(对齐 `[JsonIgnore]`)。
```cangjie
@Ignore public var _temp: String = ""
```
#### 默认必填 / @NotRequired
**默认所有 `var` 字段必填**:反序列化时要求 JSON 对象**必须包含每个 `var` 字段的键**,
否则抛异常。值可为 `null`(`null` 保持字段默认值,不抛异常);只有**键缺失**才抛异常。
仅对反序列化生效,序列化侧不受影响。
标注 `@NotRequired` 的字段允许键缺失(缺失时保持字段默认值,不抛异常):
```cangjie
@NotRequired public var _nickname: String = ""
```
- 普通类:`JsonSerializer.deserialize<T>(json)` 在 `readObject` 阶段检测,必填键缺失抛异常。
- `@SerializerParent` 父类:宏生成的 `importJsonFields` 检测父类必填键,缺失抛异常并沿继承链上抛。
## 父类字段序列化(@SerializerParent)
### 背景
@@ -100,7 +139,7 @@ let u = User()
u._id = "u-1"
u._name = "alice"
let json = JsonSerializer.Serialize(u)
let json = JsonSerializer.serialize(u)
// 输出包含父类字段:{ "_name": "alice", "_id": "u-1", "_createdAt": "" }
```
@@ -112,7 +151,7 @@ let json = JsonSerializer.Serialize(u)
|---|---|
| 自动补 `open` | 非 open 类标注宏后自动可继承 |
| 多层继承 | 沿 `superClass` 链逐层调用;中间层无宏自动跳过 |
| 注解生效 | 父类字段的 `@SerializerPropertyName` / `@SerializerIgnore` 同样生效 |
| 注解生效 | 父类字段的 `@PropertyName` / `@Ignore` / `@NotRequired` 同样生效 |
| 任意字段名 | `_` 前缀与无下划线字段都处理 |
| 向后兼容 | 无宏的类行为不变 |
@@ -137,7 +176,7 @@ let opt = JsonOption()
opt.propertyNamingPolicy = PropertyNamingPolicy.SnakeCase // userName → user_name
opt.maxDepth = 16
let json = JsonSerializer.Serialize(u, opt)
let json = JsonSerializer.serialize(u, opt)
```
### 运行时类型反序列化(框架绑定用)
@@ -148,7 +187,7 @@ let json = JsonSerializer.Serialize(u, opt)
import std.reflect.*
let typeInfo = TypeInfo.of<MyDto>()
let obj: Any = JsonSerializer.Deserialize(typeInfo, jsonString)
let obj: Any = JsonSerializer.deserialize(typeInfo, jsonString)
```
### 命名策略(PropertyNamingPolicy)
@@ -174,8 +213,8 @@ let obj: Any = JsonSerializer.Deserialize(typeInfo, jsonString)
### 宽松类型转换(反序列化)
JSON 数字 ↔ 字符串字段自动互转:`Deserialize<String>("42")` → `"42"`,
`Deserialize<Int64>("\"42\"")` → `42`。
JSON 数字 ↔ 字符串字段自动互转:`deserialize<String>("42")` → `"42"`,
`deserialize<Int64>("\"42\"")` → `42`。
## 已知限制
@@ -190,24 +229,24 @@ JSON 数字 ↔ 字符串字段自动互转:`Deserialize<String>("42")` → `"
src/
├── SimApiSerialization.cj // 根锚点:public import common.* + json.*
├── common/ // XML/JSON 共用
│ ├── Annotations.cj // @SerializerPropertyName / @SerializerIgnore
│ ├── Annotations.cj // @PropertyName / @Ignore / @NotRequired
│ ├── ReflectionCache.cj // 字段元数据 + 继承链缓存
│ └── NamingPolicy.cj // PropertyNamingPolicy
├── json/ // JSON 序列化
│ ├── JsonSerializer.cj // 公开 API(含 TypeInfo 版 Deserialize)
│ ├── JsonSerializer.cj // 公开 API(含 TypeInfo 版 deserialize)
│ ├── JsonWriter.cj / JsonReader.cj
│ └── JsonOption.cj
├── macros/ // @SerializerParent 宏
│ └── SerializerParentMacro.cj
└── tests/ // 单元测试(独立子包)
└── JsonSerializer_test.cj // cjpm test,18 用例
└── JsonSerializer_test.cj // cjpm test,21 用例
```
## 测试
```bash
cjpm test
# TOTAL: 18, PASSED: 18
# TOTAL: 21, PASSED: 21
```
覆盖:基础类型、对象字段输出、注解、Option、集合、HashMap、枚举、命名策略、
@@ -215,7 +254,7 @@ cjpm test
## 设计参考
- API 对齐 .NET `System.Text.Json`(`JsonSerializer.Serialize/Deserialize` + `JsonSerializerOptions`)
- 注解对齐 `[JsonPropertyName]` / `[JsonIgnore]`
- API 对齐 .NET `System.Text.Json`(`JsonSerializer.serialize/deserialize` + `JsonSerializerOptions`)
- 注解对齐 `[JsonPropertyName]` / `[JsonIgnore]`;默认必填语义为库自身约定,`@NotRequired` 允许字段缺失
- 父类方案参考 soulsoft_serialization 的「宏生成静态类型字段代码」思路
(详见 `docs/父类字段序列化方案.md`)
+1 -1
View File
@@ -3,7 +3,7 @@ cjc-version = "1.1.3"
name = "serialization"
organization = "simcu"
description = "SimApi 全反射 JSON 序列化库"
version = "1.1.0"
version = "1.2.0"
target-dir = ""
output-type = "static"
+8 -8
View File
@@ -183,7 +183,7 @@ import std.collection.*
public macro SerializerParent(input: Tokens): Tokens {
// 1. 解析输入的 ClassDecl(非类声明 → 抛 MacroException)
// 2. 校验 open:非 open class → 抛 MacroException(见 3.1.1)
// 3. 收集 public var 字段(跳过 @SerializerIgnore)
// 3. 收集 public var 字段(跳过 @Ignore)
// 4. 生成 exportJsonFields / importJsonFields 两个静态方法
// 5. 拼回原类声明 + 新方法,返回 Tokens
}
@@ -243,7 +243,7 @@ if (!isOpenClass(decl)) {
open class BaseUser {
public var _id: String = ""
public var _createdAt: String = ""
@SerializerIgnore
@Ignore
public var _temp: String = ""
}
```
@@ -254,7 +254,7 @@ open class BaseUser {
open class BaseUser {
public var _id: String = ""
public var _createdAt: String = ""
@SerializerIgnore
@Ignore
public var _temp: String = ""
/// 导出:字段名 → 值(静态类型访问)
@@ -262,7 +262,7 @@ open class BaseUser {
var m = HashMap<String, Any>()
m["_id"] = instance._id
m["_createdAt"] = instance._createdAt
// _temp 被 @SerializerIgnore 跳过
// _temp 被 @Ignore 跳过
m
}
@@ -318,8 +318,8 @@ Cangjie 宏需要两步编译(`cjc --compile-macro` 先编宏,再编使用
| 细节 | 处理 |
|---|---|
| 字段类型 | 用 `TypeInfo.get(字段类型名)` 在运行时做类型转换(import 侧) |
| `@SerializerPropertyName` | export 键用注解名;import 匹配也用注解名 |
| `@SerializerIgnore` | 直接跳过,不生成导出/导入语句 |
| `@PropertyName` | export 键用注解名;import 匹配也用注解名 |
| `@Ignore` | 直接跳过,不生成导出/导入语句 |
| `let`(不可变)字段 | 只导出,不导入(导入侧跳过) |
| 无字段 | 生成空方法(返回空 map / 空操作),保证约定方法一定存在 |
| 命名冲突 | 若用户已手写同名静态方法,宏应报错或跳过(设计取舍:报错更安全) |
@@ -474,7 +474,7 @@ main() {
| 用例 | 断言 |
|---|---|
| 父类字段序列化 | 输出包含 `_id`/`_createdAt`,且 `@SerializerIgnore` 父类字段不输出 |
| 父类字段序列化 | 输出包含 `_id`/`_createdAt`,且 `@Ignore` 父类字段不输出 |
| 父类字段反序列化 | `Deserialize<User>` 后父类字段值正确 |
| 多层继承 | GrandParent → Parent → Child 三层字段全部输出 |
| 中间层无宏 | 三层链中 Parent 不标宏:`_gp` 仍导出,`_p` 不导出(逐层跳过) |
@@ -482,7 +482,7 @@ main() {
| 子类字段与父类字段重名 | 子类优先(输出一份,值取子类的) |
| 父类嵌套对象字段 | 父类字段值是对象时正确递归 |
| 父类字段 null | 导入时保持默认值 |
| 与 @SerializerPropertyName 组合 | 父类字段用注解名输出/匹配 |
| 与 @PropertyName 组合 | 父类字段用注解名输出/匹配 |
| 顶层序列化父类类型实例 | `Serialize(BaseUser 实例)` 直接可用 |
| **宏自动补 open** | `@SerializerParent class Base`(无 open)展开后子类可继承(编译通过) |
| **宏严格模式报错** | `@SerializerParent[requireOpen: true]` 标注非 open class 编译失败 |
+11 -3
View File
@@ -3,7 +3,7 @@
*
* simapi_serialization 根包锚点:聚合重导出,`import simcu::serialization.*`
* 即可使用全部能力:
* - 注解(@SerializerPropertyName / @SerializerIgnore) → common 子包
* - 注解(@PropertyName / @Ignore / @NotRequired) → common 子包
* - 序列化器(JsonSerializer / JsonOption) → json 子包
*
* 子包结构:
@@ -14,5 +14,13 @@
package simcu::serialization
public import simcu::serialization.common.*
public import simcu::serialization.json.*
public import simcu::serialization.common.PropertyName
public import simcu::serialization.common.Ignore
public import simcu::serialization.common.NotRequired
public import simcu::serialization.common.FieldMetadata
public import simcu::serialization.common.ReflectionCache
public import simcu::serialization.common.PropertyNamingPolicy
public import simcu::serialization.json.JsonSerializer
public import simcu::serialization.json.JsonOption
public import simcu::serialization.json.JsonReader
public import simcu::serialization.json.JsonWriter
+21 -6
View File
@@ -1,8 +1,10 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 序列化注解(对齐 .NET System.Text.Json.Serialization 特性):
* - [JsonPropertyName("xxx")] → @SerializerPropertyName["xxx"] 指定字段的序列化名称
* - [JsonIgnore] → @SerializerIgnore 序列化/反序列化时忽略该字段
* - [JsonPropertyName("xxx")] → @PropertyName["xxx"] 指定字段的序列化名称
* - [JsonIgnore] → @Ignore 序列化/反序列化时忽略该字段
* - 默认必填语义 → 反序列化时要求每个字段的键必须存在;
* `@NotRequired` → 标注的字段允许缺失(键不存在不报错,保持默认值)
*
* 设计决策:本库序列化/反序列化只处理「var 成员变量」(std.reflect 的 instanceVariables),
* 不处理 prop 属性(instanceProperties)。因此注解 target 仅限 MemberVariable:
@@ -13,10 +15,10 @@ package simcu::serialization.common
/**
* 指定字段的序列化名称(对齐 .NET JsonPropertyNameAttribute)。
* 用法:@SerializerPropertyName["user_name"] public var _username: String = ""
* 用法:@PropertyName["user_name"] public var _username: String = ""
*/
@Annotation[target: [MemberVariable]]
public class SerializerPropertyName {
public class PropertyName {
public let name: String
public const init(name: String) {
@@ -26,9 +28,22 @@ public class SerializerPropertyName {
/**
* 标记字段不做序列化处理(对齐 .NET JsonIgnoreAttribute)。
* 用法:@SerializerIgnore public var _temp: String = ""
* 用法:@Ignore public var _temp: String = ""
*/
@Annotation[target: [MemberVariable]]
public class SerializerIgnore {
public class Ignore {
public const init() {}
}
/**
* 标记字段在反序列化时「允许缺失」(默认语义是全部字段必填)。
* 语义:本库默认要求反序列化的 JSON 对象中必须包含每个 var 字段的键;
* 标注 @NotRequired 后,该字段的键可以缺失(缺失时保持字段默认值,不抛异常)。
* 若键存在但值为 null,仍视为「存在」(保持默认值,不抛异常)。
* 仅对反序列化生效,序列化侧不受影响。
* 用法:@NotRequired public var _name: String = ""
*/
@Annotation[target: [MemberVariable]]
public class NotRequired {
public const init() {}
}
+30 -22
View File
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* ReflectionCache:字段元数据(含继承链、命名策略、@SerializerPropertyName/@SerializerIgnore)与缓存。
* ReflectionCache:字段元数据(含继承链、命名策略、@PropertyName/@Ignore/@NotRequired)与缓存。
*
* 已知限制(平台级):
* - InstanceVariableInfo.getValue/setValue 有「声明类严格类型校验」,父类字段无法用子类实例读写,
@@ -9,8 +9,11 @@
package simcu::serialization.common
import std.collection.*
import std.reflect.*
import std.collection.ArrayList
import std.collection.HashMap
import std.reflect.ClassTypeInfo
import std.reflect.InstanceVariableInfo
import std.reflect.TypeInfo
/**
* 单个字段的序列化元数据。
@@ -18,10 +21,12 @@ import std.reflect.*
public class FieldMetadata {
/// 反射字段名(如 _name)
public var name: String = ""
/// JSON 名称(@SerializerPropertyName 优先,否则按命名策略)
/// JSON 名称(@PropertyName 优先,否则按命名策略)
public var jsonName: String = ""
/// 是否忽略(@SerializerIgnore)
/// 是否忽略(@Ignore)
public var ignore: Bool = false
/// 是否必填(默认 true:反序列化时要求该字段必须存在;@NotRequired 则允许缺失)
public var required: Bool = true
/// 字段类型
public var typeInfo: ?TypeInfo = None
/// 字段读写句柄
@@ -36,25 +41,25 @@ public class FieldMetadata {
public class ReflectionCache {
private init() {}
private static let _cache = HashMap<String, ArrayList<FieldMetadata>>()
private static let _CACHE = HashMap<String, ArrayList<FieldMetadata>>()
/// 类型 → 是否有 @SerializerParent 宏生成的 exportJsonFields(Bool)
private static let _exportCache = HashMap<String, Bool>()
private static let _EXPORT_CACHE = HashMap<String, Bool>()
/// 类型 → 是否有 @SerializerParent 宏生成的 importJsonFields(Bool)
private static let _importCache = HashMap<String, Bool>()
private static let _IMPORT_CACHE = HashMap<String, Bool>()
/**
* 该类型是否含 @SerializerParent 宏生成的导出静态方法(含缓存)。
*/
public static func hasExportFields(typeInfo: ClassTypeInfo): Bool {
hasParentMethod(typeInfo, "exportJsonFields", _exportCache)
hasParentMethod(typeInfo, "exportJsonFields", _EXPORT_CACHE)
}
/**
* 该类型是否含 @SerializerParent 宏生成的导入静态方法(含缓存)。
*/
public static func hasImportFields(typeInfo: ClassTypeInfo): Bool {
hasParentMethod(typeInfo, "importJsonFields", _importCache)
hasParentMethod(typeInfo, "importJsonFields", _IMPORT_CACHE)
}
private static func hasParentMethod(typeInfo: ClassTypeInfo, name: String, cache: HashMap<String, Bool>): Bool {
@@ -84,7 +89,7 @@ public class ReflectionCache {
*/
public static func getFields(typeInfo: ClassTypeInfo, namingPolicy: PropertyNamingPolicy): ArrayList<FieldMetadata> {
let key = typeInfo.qualifiedName
if (let Some(cached) <- _cache.get(key)) {
if (let Some(cached) <- _CACHE.get(key)) {
return cached
}
var fields = ArrayList<FieldMetadata>()
@@ -94,13 +99,17 @@ public class ReflectionCache {
meta.variable = Some(v)
meta.typeInfo = Some(v.typeInfo)
meta.mutable = v.isMutable()
// @SerializerIgnore
if (v.findAnnotation<SerializerIgnore>().isSome()) {
// @Ignore
if (v.findAnnotation<Ignore>().isSome()) {
meta.ignore = true
}
// @SerializerPropertyName 优先
// @NotRequired:反序列化时允许该字段缺失(默认所有字段必填)
if (v.findAnnotation<NotRequired>().isSome()) {
meta.required = false
}
// @PropertyName 优先
var jsonName = ""
if (let Some(jsonProp) <- v.findAnnotation<SerializerPropertyName>()) {
if (let Some(jsonProp) <- v.findAnnotation<PropertyName>()) {
jsonName = jsonProp.name
} else {
jsonName = applyNamingPolicy(v.name, namingPolicy)
@@ -108,7 +117,7 @@ public class ReflectionCache {
meta.jsonName = jsonName
fields.add(meta)
}
_cache[key] = fields
_CACHE[key] = fields
fields
}
@@ -138,19 +147,18 @@ public class ReflectionCache {
return s
}
var sb = StringBuilder()
var first = true
for (c in s.runes()) {
// 首字符单独处理:大写转小写且不加前置下划线
sb.append(s[0..1].toAsciiLower())
let rest = s[1..]
for (c in rest.runes()) {
let ch = UInt32(c)
let isUpper = ch >= 0x41 && ch <= 0x5A
if (isUpper) {
if (!first) {
sb.append("_")
}
sb.append("_")
sb.append(Rune(ch + 0x20))
} else {
sb.append(c)
}
first = false
}
sb.toString()
}
+3 -3
View File
@@ -1,11 +1,11 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* JsonOption:序列化选项(对齐 .NET JsonSerializerOptions)。
* JsonOption:序列化选项。
*/
package simcu::serialization.json
import simcu::serialization.common.*
import simcu::serialization.common.PropertyNamingPolicy
/**
* 序列化选项。
@@ -21,5 +21,5 @@ public class JsonOption {
public var maxDepth: Int64 = 64
/// 默认选项实例
public static let instance: JsonOption = JsonOption()
public static let INSTANCE: JsonOption = JsonOption()
}
+121 -54
View File
@@ -9,11 +9,22 @@
package simcu::serialization.json
import std.collection.*
import std.convert.*
import std.reflect.*
import std.time.*
import stdx.encoding.json.*
import std.collection.ArrayList
import std.collection.HashMap
import std.convert.Parsable
import std.reflect.ClassTypeInfo
import std.reflect.EnumTypeInfo
import std.reflect.InstanceFunctionInfo
import std.reflect.TypeInfo
import std.time.DateTime
import stdx.encoding.json.JsonArray
import stdx.encoding.json.JsonBool
import stdx.encoding.json.JsonFloat
import stdx.encoding.json.JsonInt
import stdx.encoding.json.JsonNull
import stdx.encoding.json.JsonObject
import stdx.encoding.json.JsonString
import stdx.encoding.json.JsonValue
import simcu::serialization.common.ReflectionCache
import simcu::serialization.common.FieldMetadata
@@ -23,6 +34,13 @@ import simcu::serialization.common.FieldMetadata
public class JsonReader {
private init() {}
/**
* 反序列化 JSON 值为目标类型 T。
* @param value JSON 值。
* @param options 序列化选项。
* @return 目标类型实例。
* @throws Exception 结果类型不匹配或反序列化失败时。
*/
public static func read<T>(value: JsonValue, options: JsonOption): T {
let result = readValue(value, TypeInfo.of<T>(), options, 0)
if (let t: T <- result) {
@@ -37,11 +55,16 @@ public class JsonReader {
* @param value JSON 值。
* @param options 序列化选项。
* @return 反序列化后的实例(Any)。
* @throws Exception 反序列化失败时。
*/
public static func read(typeInfo: TypeInfo, value: JsonValue, options: JsonOption): Any {
readValue(value, typeInfo, options, 0)
}
/**
* 按类型信息递归反序列化。
* @throws Exception 深度超限、类型不支持或转换失败时。
*/
private static func readValue(value: JsonValue, typeInfo: TypeInfo, options: JsonOption, depth: Int64): Any {
if (depth > options.maxDepth) {
throw Exception("JsonSerializer: 超过最大递归深度 ${options.maxDepth}")
@@ -83,11 +106,12 @@ public class JsonReader {
return noneVal
}
let innerName = JsonWriter.extractTypeArgs(typeName)[0]
let innerVal = readValue(value, TypeInfo.get(innerName), options, depth + 1)
if (let et: EnumTypeInfo <- typeInfo) {
let innerVal = readValue(value, TypeInfo.get(innerName), options, depth + 1)
let ctor = et.getConstructor("Some", argsCount: 1)
return ctor.apply([innerVal])
}
let innerVal = readValue(value, TypeInfo.get(innerName), options, depth + 1)
return Some(innerVal)
}
@@ -131,7 +155,6 @@ public class JsonReader {
private static func readObject(value: JsonValue, ct: ClassTypeInfo, options: JsonOption, depth: Int64): Any {
let instance = ct.construct([])
let fields = ReflectionCache.getFields(ct, options.propertyNamingPolicy)
let jobj = value.asObject()
for (f in fields) {
if (f.ignore) {
continue
@@ -139,14 +162,18 @@ public class JsonReader {
if (!f.mutable) {
continue
}
match (jobj.get(f.jsonName)) {
match (value.asObject().get(f.jsonName)) {
case Some(jv) =>
if (jv is JsonNull) {
continue // null:跳过,保持字段默认值
}
let fieldVal = readValue(jv, f.typeInfo.getOrThrow(), options, depth + 1)
f.variable.getOrThrow().setValue(instance, fieldVal)
case None => ()
case None =>
// 默认必填:JSON 键必须存在,否则抛异常(@NotRequired 字段已置 required=false)
if (f.required) {
throw Exception("JsonSerializer: 缺少必填字段 \"${f.jsonName}\"")
}
}
}
// 父类字段(@SerializerParent 宏生成的静态导入方法,沿继承链逐层回填)
@@ -158,7 +185,6 @@ public class JsonReader {
/// 无宏的父类(hasImportFields 缓存判断)静默跳过;宏生成的方法内部按
/// if (k == "字段名") 忽略未知键,因此直接把全部 JSON 键传入即可。
private static func readParentFields(value: JsonValue, ct: ClassTypeInfo, instance: Any): Unit {
let jobj = value.asObject()
var parent = ct.superClass
while (let Some(p) <- parent) {
if (ReflectionCache.hasImportFields(p)) {
@@ -166,95 +192,118 @@ public class JsonReader {
let fn = p.getStaticFunction("importJsonFields",
[p, TypeInfo.of<HashMap<String, Any>>()])
var pjson = HashMap<String, Any>()
for ((k, jv) in jobj.getFields()) {
if (!(jv is JsonNull)) {
pjson[k] = jsonToAny(jv)
}
for ((k, jv) in value.asObject().getFields()) {
// 保留所有键(含 null 值),使默认必填的「键必须存在」判定与
// 反射路径一致:null 值字段仍视为「存在」,导入时按类型匹配失败跳过
pjson[k] = jsonToAny(jv)
}
let args: Array<Any> = [instance, pjson]
fn.apply(p, args)
} catch (_: Exception) {
// 导入失败(如类型不匹配)不中断整体反序列化
} catch (e: Exception) {
// 默认必填字段缺失 → 中断整体反序列化
if (e.message.contains("缺少必填字段")) {
throw e
}
// 其余导入失败(如类型不匹配)不中断整体反序列化
let _ = e
}
}
parent = p.superClass
}
}
/// Array<X>:经 ArrayList<X> 构造 + add + toArray
/// Array<X>:经 ArrayList<X> 构造 + add + toArray。
/// @throws Exception 目标类型解析失败时。
private static func readArray(value: JsonValue, elemName: String, options: JsonOption, depth: Int64): Any {
let listTypeName = "std.collection.ArrayList<${elemName}>"
let listCt = (TypeInfo.get(listTypeName) as ClassTypeInfo) ?? throw Exception("JsonSerializer: 无法解析类型 ${listTypeName}")
let list = listCt.construct([])
let addFunc = listCt.getInstanceFunction("add", [TypeInfo.get(elemName)])
let jarr = value.asArray()
for (item in jarr.getItems()) {
let elem = readValue(item, TypeInfo.get(elemName), options, depth + 1)
addFunc.apply(list, [elem])
}
fillCollection(list, addFunc, jarr, elemName, options, depth)
let toArrayFunc = listCt.getInstanceFunction("toArray", [])
toArrayFunc.apply(list, [])
}
/// 反射批量填充单值集合(Array/ArrayList/HashSet)。
/// addFunc 为只查询一次的 add 函数句柄,作为参数传入以遵循最小作用域。
private static func fillCollection(collection: Any, addFunc: InstanceFunctionInfo, jarr: JsonArray, elemName: String,
options: JsonOption, depth: Int64): Unit {
for (item in jarr.getItems()) {
let elem = readValue(item, TypeInfo.get(elemName), options, depth + 1)
addFunc.apply(collection, [elem])
}
}
/// 反序列化 ArrayList<X>。
/// @throws Exception 目标类型解析失败时。
private static func readArrayList(value: JsonValue, elemName: String, options: JsonOption, depth: Int64): Any {
let ct = (TypeInfo.get("std.collection.ArrayList<${elemName}>") as ClassTypeInfo) ?? throw Exception("JsonSerializer: 无法解析 ArrayList<${elemName}>")
let list = ct.construct([])
let addFunc = ct.getInstanceFunction("add", [TypeInfo.get(elemName)])
let jarr = value.asArray()
for (item in jarr.getItems()) {
let elem = readValue(item, TypeInfo.get(elemName), options, depth + 1)
addFunc.apply(list, [elem])
}
fillCollection(list, addFunc, jarr, elemName, options, depth)
list
}
/// 反序列化 HashSet<X>。
/// @throws Exception 目标类型解析失败时。
private static func readHashSet(value: JsonValue, elemName: String, options: JsonOption, depth: Int64): Any {
let ct = (TypeInfo.get("std.collection.HashSet<${elemName}>") as ClassTypeInfo) ?? throw Exception("JsonSerializer: 无法解析 HashSet<${elemName}>")
let set = ct.construct([])
let addFunc = ct.getInstanceFunction("add", [TypeInfo.get(elemName)])
let jarr = value.asArray()
for (item in jarr.getItems()) {
let elem = readValue(item, TypeInfo.get(elemName), options, depth + 1)
addFunc.apply(set, [elem])
}
fillCollection(set, addFunc, jarr, elemName, options, depth)
set
}
/// 反序列化 HashMap<K, V>。
/// @throws Exception 目标类型解析失败时。
private static func readHashMap(value: JsonValue, keyName: String, valName: String, options: JsonOption,
depth: Int64): Any {
let ct = (TypeInfo.get("std.collection.HashMap<${keyName}, ${valName}>") as ClassTypeInfo) ?? throw Exception("JsonSerializer: 无法解析 HashMap<${keyName}, ${valName}>")
let map = ct.construct([])
let addFunc = ct.getInstanceFunction("add", [TypeInfo.get(keyName), TypeInfo.get(valName)])
let jobj = value.asObject()
fillHashMap(map, addFunc, jobj, keyName, valName, options, depth)
map
}
/// 反射批量填充 HashMap。addFunc 为只查询一次的 add 函数句柄,作为参数传入以遵循最小作用域。
private static func fillHashMap(map: Any, addFunc: InstanceFunctionInfo, jobj: JsonObject, keyName: String,
valName: String,
options: JsonOption, depth: Int64): Unit {
for ((k, jv) in jobj.getFields()) {
let keyVal = readValue(JsonString(k), TypeInfo.get(keyName), options, depth + 1)
let valVal = readValue(jv, TypeInfo.get(valName), options, depth + 1)
addFunc.apply(map, [keyVal, valVal])
}
map
}
/// JsonValue → 通用 Any(动态结构)
private static func jsonToAny(v: JsonValue): Any {
match (v.kind()) {
case JsNull => noneAny()
case JsBool => v.asBool().getValue()
case JsInt => v.asInt().getValue()
case JsFloat => v.asFloat().getValue()
case JsString => v.asString().getValue()
case JsArray =>
var list = ArrayList<Any>()
for (item in v.asArray().getItems()) {
list.add(jsonToAny(item))
}
list.toArray()
case JsObject =>
var map = HashMap<String, Any>()
for ((k, val) in v.asObject().getFields()) {
map[k] = jsonToAny(val)
}
map
// 注意:不能用 v.kind() 做分派——cjc 1.1.3 会把对虚方法 kind() 的调用
// 编译期折叠为 JsNull,导致所有值被误判为 null。改用 is 类型判断。
if (let s: JsonString <- v) { return s.getValue() }
if (let b: JsonBool <- v) { return b.getValue() }
if (let i: JsonInt <- v) { return i.getValue() }
if (let f: JsonFloat <- v) { return f.getValue() }
if (v is JsonNull) { return noneAny() }
if (let a: JsonArray <- v) {
var list = ArrayList<Any>()
for (item in a.getItems()) {
list.add(jsonToAny(item))
}
return list.toArray()
}
if (let o: JsonObject <- v) {
var map = HashMap<String, Any>()
for ((k, val) in o.getFields()) {
map[k] = jsonToAny(val)
}
return map
}
noneAny()
}
private static func noneAny(): Any {
@@ -264,6 +313,8 @@ public class JsonReader {
// ===== 宽松类型转换 =====
/// 宽松转换为 String。
/// @throws Exception 无法转换时。
private static func readString(v: JsonValue): String {
if (let s: JsonString <- v) { return s.getValue() }
if (let i: JsonInt <- v) { return "${i.getValue()}" }
@@ -272,6 +323,8 @@ public class JsonReader {
throw Exception("JsonSerializer: 无法转换为 String")
}
/// 宽松转换为 Int64。
/// @throws Exception 无法转换时。
private static func readInt64(v: JsonValue): Int64 {
if (let i: JsonInt <- v) { return i.getValue() }
if (let f: JsonFloat <- v) { return Int64(f.getValue()) }
@@ -279,6 +332,8 @@ public class JsonReader {
throw Exception("JsonSerializer: 无法转换为 Int64")
}
/// 宽松转换为 Float64。
/// @throws Exception 无法转换时。
private static func readFloat64(v: JsonValue): Float64 {
if (let f: JsonFloat <- v) { return f.getValue() }
if (let i: JsonInt <- v) { return Float64(i.getValue()) }
@@ -286,22 +341,32 @@ public class JsonReader {
throw Exception("JsonSerializer: 无法转换为 Float64")
}
/// 宽松转换为 Bool。
/// @throws Exception 无法转换时。
private static func readBool(v: JsonValue): Bool {
if (let b: JsonBool <- v) { return b.getValue() }
if (let s: JsonString <- v) { return s.getValue() == "true" }
throw Exception("JsonSerializer: 无法转换为 Bool")
}
/// 取首字符作为 Rune。
/// @throws Exception 字符串为空无法转换时。
private static func readChar(v: JsonValue): Rune {
let s = readString(v)
for (r in s.runes()) {
for (r in readString(v).runes()) {
return r
}
throw Exception("JsonSerializer: 无法转换为 Char")
}
/// 解析 DateTime(格式 yyyy-MM-dd HH:mm:ss)。
/// @throws Exception 格式不匹配无法转换时。
private static func readDateTime(v: JsonValue): DateTime {
let s = readString(v)
parseDateTime(readString(v))
}
/// 严格解析 DateTime,失败时抛出带原始字符串的错误信息。
/// @throws Exception 格式不匹配无法转换时。
private static func parseDateTime(s: String): DateTime {
try {
return DateTime.parse(s, "yyyy-MM-dd HH:mm:ss")
} catch (_: Exception) {
@@ -310,6 +375,7 @@ public class JsonReader {
}
/// 生成某类型的占位默认值(用于构造 Option<X> 的 None:None 的参数槽类型是 X)。
/// @throws Exception 无法构造占位值时。
private static func defaultValueFor(typeName: String): Any {
match (typeName) {
case "String" => return ""
@@ -334,9 +400,10 @@ public class JsonReader {
}
if (let et: EnumTypeInfo <- TypeInfo.get(typeName)) {
// constructors 是 Collection:取第一个构造器名
let first = et.constructors.iterator().next().getOrThrow()
let ctor = et.getConstructor(first.name, argsCount: 0)
return ctor.apply([])
if (let Some(first) <- et.constructors.iterator().next()) {
let ctor = et.getConstructor(first.name, argsCount: 0)
return ctor.apply([])
}
}
throw Exception("JsonSerializer: 无法为 Option 占位值构造类型 ${typeName}")
}
+39 -21
View File
@@ -1,19 +1,20 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* JsonSerializer:公开 API,对齐 .NET System.Text.Json:
* JsonSerializer.Serialize(obj, JsonOption) → JSON 字符串
* JsonSerializer.Deserialize<T>(jsonString, JsonOption) → T
* JsonSerializer:公开 API:
* serialize(obj, JsonOption) → JSON 字符串
* deserialize<T>(jsonString, JsonOption) → T
* deserialize<T>(typeInfo, jsonString, JsonOption) → T(运行时类型信息绑定)
*
* 全反射实现:任意类(无参构造 + var 字段)无需实现接口、无需宏。
*/
package simcu::serialization.json
import std.reflect.*
import stdx.encoding.json.*
import std.reflect.TypeInfo
import stdx.encoding.json.JsonValue
/**
* 全反射 JSON 序列化器(对齐 .NET JsonSerializer)。
* 全反射 JSON 序列化器。
*/
public class JsonSerializer {
private init() {}
@@ -21,54 +22,71 @@ public class JsonSerializer {
/**
* 序列化任意对象为 JSON 字符串。
* @param obj 任意对象(基础类型/集合/HashMap/枚举/普通类)。
* @param options 序列化选项(默认 JsonOption.instance)。
* @param options 序列化选项(默认 JsonOption.INSTANCE)。
* @return JSON 字符串。
* @throws Exception 类型不支持或超过递归深度时抛出。
*/
public static func Serialize(obj: Any, options: JsonOption): String {
public static func serialize<T>(obj: T, options: JsonOption): String {
JsonWriter.write(obj, options).toJsonString()
}
/**
* 序列化任意对象为 JSON 字符串(默认选项)。
* @param obj 任意对象。
* @return JSON 字符串。
* @throws Exception 类型不支持或超过递归深度时抛出。
*/
public static func Serialize(obj: Any): String {
Serialize(obj, JsonOption.instance)
public static func serialize<T>(obj: T): String {
serialize(obj, JsonOption.INSTANCE)
}
/**
* 反序列化 JSON 字符串为目标类型。
* @param jsonString JSON 字符串。
* @param options 序列化选项(默认 JsonOption.instance)。
* @param options 序列化选项(默认 JsonOption.INSTANCE)。
* @return T 实例(要求 T 有无参构造,字段为 var)。
* @throws Exception JSON 格式非法、类型不匹配或目标类型不支持时抛出。
*/
public static func Deserialize<T>(jsonString: String, options: JsonOption): T {
public static func deserialize<T>(jsonString: String, options: JsonOption): T {
let value = JsonValue.fromStr(jsonString)
JsonReader.read<T>(value, options)
}
/**
* 反序列化 JSON 字符串为目标类型(默认选项)。
* @param jsonString JSON 字符串。
* @return T 实例(要求 T 有无参构造,字段为 var)。
* @throws Exception JSON 格式非法、类型不匹配或目标类型不支持时抛出。
*/
public static func Deserialize<T>(jsonString: String): T {
Deserialize<T>(jsonString, JsonOption.instance)
public static func deserialize<T>(jsonString: String): T {
deserialize<T>(jsonString, JsonOption.INSTANCE)
}
/**
* 按运行时类型信息反序列化(供框架运行时绑定等场景使用,无需编译期泛型)。
* @param typeInfo 目标类型(运行时)。
* 按运行时类型信息反序列化(供框架运行时绑定等场景使用)。
* @param typeInfo 目标类型(运行时,决定构造哪种类型)。
* @param jsonString JSON 字符串。
* @param options 序列化选项。
* @return 反序列化后的实例(Any)。
* @return T 实例;若实际结果与 T 不匹配抛出异常。
* @throws Exception JSON 格式非法、类型不匹配或目标类型不支持时抛出。
*/
public static func Deserialize(typeInfo: TypeInfo, jsonString: String, options: JsonOption): Any {
public static func deserialize<T>(typeInfo: TypeInfo, jsonString: String, options: JsonOption): T {
let value = JsonValue.fromStr(jsonString)
JsonReader.read(typeInfo, value, options)
let result = JsonReader.read(typeInfo, value, options)
if (let t: T <- result) {
return t
}
throw Exception("JsonSerializer: 运行时绑定反序列化结果类型不匹配")
}
/**
* 按运行时类型信息反序列化(默认选项)。
* @param typeInfo 目标类型(运行时,决定构造哪种类型)。
* @param jsonString JSON 字符串。
* @return T 实例;若实际结果与 T 不匹配抛出异常。
* @throws Exception JSON 格式非法、类型不匹配或目标类型不支持时抛出。
*/
public static func Deserialize(typeInfo: TypeInfo, jsonString: String): Any {
Deserialize(typeInfo, jsonString, JsonOption.instance)
public static func deserialize<T>(typeInfo: TypeInfo, jsonString: String): T {
deserialize<T>(typeInfo, jsonString, JsonOption.INSTANCE)
}
}
+100 -51
View File
@@ -8,10 +8,21 @@
package simcu::serialization.json
import std.collection.*
import std.reflect.*
import std.time.*
import stdx.encoding.json.*
import std.collection.ArrayList
import std.collection.HashMap
import std.reflect.ClassTypeInfo
import std.reflect.EnumTypeInfo
import std.reflect.InstanceFunctionInfo
import std.reflect.TypeInfo
import std.time.DateTime
import stdx.encoding.json.JsonArray
import stdx.encoding.json.JsonBool
import stdx.encoding.json.JsonFloat
import stdx.encoding.json.JsonInt
import stdx.encoding.json.JsonNull
import stdx.encoding.json.JsonObject
import stdx.encoding.json.JsonString
import stdx.encoding.json.JsonValue
import simcu::serialization.common.ReflectionCache
import simcu::serialization.common.FieldMetadata
@@ -25,6 +36,10 @@ public class JsonWriter {
writeValue(obj, options, 0)
}
/**
* 将任意值递归写为 JsonValue。
* @throws Exception 深度超限或类型不支持时。
*/
private static func writeValue(value: Any, options: JsonOption, depth: Int64): JsonValue {
if (depth > options.maxDepth) {
throw Exception("JsonSerializer: 超过最大递归深度 ${options.maxDepth}")
@@ -105,17 +120,26 @@ public class JsonWriter {
obj
}
/**
* 反射写入对象字段。
* @throws Exception 字段缺少变量句柄时。
*/
private static func writeObject(value: Any, ct: ClassTypeInfo, options: JsonOption, depth: Int64): JsonValue {
let obj = JsonObject()
// 1) 子类自身字段(反射)
let fields = ReflectionCache.getFields(ct, options.propertyNamingPolicy)
for (f in fields) {
// 只要不是 @SerializerIgnore 的字段一律输出;None 字段输出 null
// 只要不是 @Ignore 的字段一律输出;None 字段输出 null
if (f.ignore) {
continue
}
let raw = f.variable.getOrThrow().getValue(value)
obj.put(f.jsonName, writeValue(raw, options, depth + 1))
match (f.variable) {
case Some(variable) =>
let raw = variable.getValue(value)
obj.put(f.jsonName, writeValue(raw, options, depth + 1))
case None =>
throw Exception("JsonSerializer: 字段 ${f.jsonName} 缺少变量句柄")
}
}
// 2) 父类字段(@SerializerParent 宏生成的静态导出方法,沿继承链逐层调用)
writeParentFields(value, ct, obj, options, depth)
@@ -138,45 +162,62 @@ public class JsonWriter {
obj.put(k, writeValue(v, options, depth + 1))
}
}
} catch (_: Exception) {
} catch (e: Exception) {
// 调用失败(不应发生)→ 跳过该层
let _ = e
}
}
parent = p.superClass
}
}
/// 反射迭代集合:优先 toArray 转数组再 get(index);无 toArray(如 Array)直接用原集合
/// 反射迭代集合:优先 toArray 转数组再 get(index);无 toArray(如 Array)直接用原集合。
/// @throws Exception 集合迭代超限(超过 100 万次)时。
private static func forEachElement(collection: Any, action: (Any) -> Unit): Unit {
let ti = TypeInfo.of(collection)
var arr: Any = collection
try {
let ti = TypeInfo.of(collection)
arr = ti.getInstanceFunction("toArray", []).apply(collection, [])
} catch (_: Exception) {
} catch (e: Exception) {
// 无 toArray(如 Array):直接用原集合 get(i)
let _ = e
}
let arrTi = TypeInfo.of(arr)
// 仓颉集合 get(index) 返回 Option<T>:Some(元素) / None(越界结束)
let getFunc = arrTi.getInstanceFunction("get", [TypeInfo.of<Int64>()])
iterateElements(arr, getFunc, action)
}
/// 反射迭代集合元素:get(i) 越界(抛异常或返回 None)视为迭代结束;
/// 超过 100 万次仍不结束抛异常。getFunc 为只查询一次的 get 函数句柄,作为参数传入。
private static func iterateElements(arr: Any, getFunc: InstanceFunctionInfo, action: (Any) -> Unit): Unit {
var i: Int64 = 0
var guard: Int64 = 0
while (true) {
guard += 1
if (guard > 1000000) {
throw Exception("JsonSerializer: 集合迭代超限 type=${TypeInfo.of(arr)}")
var done = false
while (i < 1000000 && !done) {
match (getElemAt(getFunc, arr, i)) {
case None => done = true
case Some(inner) =>
if (let Some(v) <- unwrapOptionValue(inner)) {
action(v)
} else {
done = true
}
}
var elem: ?Any = None
try {
elem = Some(getFunc.apply(arr, [i]))
} catch (_: Exception) {
break
if (!done) {
i += 1
}
if (let Some(v) <- unwrapOptionValue(elem.getOrThrow())) {
action(v)
} else {
break
}
i += 1
}
if (i >= 1000000 && !done) {
throw Exception("JsonSerializer: 集合迭代超限 type=${TypeInfo.of(arr)}")
}
}
/// 反射 get(i):越界抛异常视为 None(迭代结束)。
private static func getElemAt(getFunc: InstanceFunctionInfo, arr: Any, k: Int64): ?Any {
try {
Some(getFunc.apply(arr, [k]))
} catch (e: Exception) {
None
}
}
@@ -203,32 +244,40 @@ public class JsonWriter {
/// 从 "Array<String>" / "std.collection.HashMap<String, Any>" / "Option<Int64>" 提取类型参数
public static func extractTypeArgs(typeName: String): ArrayList<String> {
var args = ArrayList<String>()
let args = ArrayList<String>()
match (typeName.indexOf("<")) {
case None => return args
case Some(i) =>
var depth: Int64 = 0
var start = i + 1
var j = i + 1
while (j < typeName.size) {
let b = typeName[j]
if (b == 0x3Cu8) { // '<'
depth += 1
} else if (b == 0x3Eu8) { // '>'
if (depth == 0) {
args.add(typeName[start..j])
return args
}
depth -= 1
} else if (b == 0x2Cu8) { // ','
if (depth == 0) {
args.add(typeName[start..j])
start = j + 1
}
}
j += 1
}
case None => ()
case Some(i) => scanTypeArgs(typeName, i + 1, 0, i + 1, args)
}
args
}
/// 递归逐字符扫描类型参数列表(等价于原 while 版本)。
/// j 为当前扫描位置,depth 为 '<' 嵌套深度,start 为当前待收集参数起点;
/// 遇到 depth==0 的 '>'(外层闭合)时收集最后一个参数并结束。
private static func scanTypeArgs(typeName: String, j: Int64, depth: Int64, start: Int64,
args: ArrayList<String>): Unit {
if (j >= typeName.size) {
return
}
let b = typeName[j]
if (b == 0x3Cu8) { // '<'
scanTypeArgs(typeName, j + 1, depth + 1, start, args)
} else if (b == 0x3Eu8) { // '>'
if (depth == 0) {
args.add(typeName[start..j])
return
}
scanTypeArgs(typeName, j + 1, depth - 1, start, args)
} else if (b == 0x2Cu8) { // ','
if (depth == 0) {
args.add(typeName[start..j])
scanTypeArgs(typeName, j + 1, depth, j + 1, args)
} else {
scanTypeArgs(typeName, j + 1, depth, start, args)
}
} else {
scanTypeArgs(typeName, j + 1, depth, start, args)
}
}
}
+88 -13
View File
@@ -12,17 +12,20 @@
* @SerializerParent
* class BaseUser {
* public var _id: String = ""
* @SerializerPropertyName["userId"]
* @PropertyName["userId"]
* public var _userId: String = ""
* @SerializerIgnore
* @Ignore
* public var _temp: String = ""
* @NotRequired
* public var _name: String = ""
* }
*
* 行为:
* - 非 open class 自动补 open
* - 生成 exportJsonFields(instance: T): HashMap<String, Any>
* - 生成 importJsonFields(instance: T, json: HashMap<String, Any>): Unit
* - @SerializerIgnore 字段跳过;@SerializerPropertyName 用注解名做 JSON 键
* - @Ignore 字段跳过;@PropertyName 用注解名做 JSON 键
* - 默认所有 var 字段必填(反序列化要求 JSON 键存在);@NotRequired 字段允许缺失
*/
macro package simcu::serialization.macros
@@ -36,6 +39,7 @@ private class FieldInfo {
public var jsonName: String = "" // JSON 键名(注解优先,否则字段名)
public var typeName: String = "" // 字段类型名(如 String)
public var isVar: Bool = true // var 可变(可导入);let 只导出
public var required: Bool = true // 默认必填;@NotRequired 则允许缺失
public init() {}
@@ -45,6 +49,27 @@ private class FieldInfo {
this.typeName = typeName
this.isVar = isVar
}
public init(fieldName: String, jsonName: String, typeName: String, isVar: Bool, required: Bool) {
this.fieldName = fieldName
this.jsonName = jsonName
this.typeName = typeName
this.isVar = isVar
this.required = required
}
}
/// 单个注解的信息(名称 + 参数 tokens)
private class AnnotInfo {
public var name: String = "" // 注解名(如 PropertyName)
public var attrs: String = "" // 注解参数 tokens(如 "user_alias",含引号)
public init() {}
public init(name: String, attrs: String) {
this.name = name
this.attrs = attrs
}
}
/**
@@ -85,7 +110,7 @@ public macro SerializerParent(input: Tokens): Tokens {
throw ASTException("@SerializerParent 只能标注在 class 声明上(不支持 struct/interface/enum)")
}
/// 收集类体内的可序列化字段(任意 var/let 字段 + 类型注解;跳过 @SerializerIgnore;@SerializerPropertyName 用注解名)
/// 收集类体内的可序列化字段(任意 var/let 字段 + 类型注解;跳过 @Ignore;@PropertyName 用注解名;默认必填,@NotRequired 允许缺失)
private func collectFields(cd: ClassDecl): ArrayList<FieldInfo> {
var result = ArrayList<FieldInfo>()
for (d in cd.body.decls) {
@@ -96,20 +121,25 @@ private func collectFields(cd: ClassDecl): ArrayList<FieldInfo> {
vd.declType.toTokens().toString(), vd.keyword.kind == TokenKind.VAR))
}
case md: MacroExpandDecl =>
// 带注解的字段:@SerializerIgnore 跳过;@SerializerPropertyName["x"] 取注解名
if (md.identifier.value == "SerializerIgnore") {
// 带注解的字段:@Ignore 跳过;@PropertyName["x"] 取注解名;@NotRequired 允许缺失
let annots = collectAnnotations(md)
if (hasAnnotation(annots, "Ignore")) {
continue
}
if (let Some(inner) <- unwrapMacroField(md)) {
if (isSerializableField(inner)) {
let jsonName = if (md.identifier.value == "SerializerPropertyName") {
// 属性 tokens 形如 "user_alias"(带引号),去掉两端引号
stripQuotes(md.macroAttrs.toString())
} else {
inner.identifier.value
var jsonName = inner.identifier.value
var required = true // 默认必填
for (an in annots) {
if (an.name == "PropertyName") {
// 属性 tokens 形如 "user_alias"(带引号),去掉两端引号
jsonName = stripQuotes(an.attrs)
} else if (an.name == "NotRequired") {
required = false
}
}
result.add(FieldInfo(inner.identifier.value, jsonName,
inner.declType.toTokens().toString(), inner.keyword.kind == TokenKind.VAR))
inner.declType.toTokens().toString(), inner.keyword.kind == TokenKind.VAR, required))
}
}
case _ => ()
@@ -118,6 +148,30 @@ private func collectFields(cd: ClassDecl): ArrayList<FieldInfo> {
result
}
/// 收集字段上全部注解(含嵌套,支持 @NotRequired 与 @PropertyName 组合)。
/// 手动记录最外层注解后再遍历内部,避免遍历器是否含根节点的不确定性;
/// 重复收集同名注解对后续「是否存在」与「取哪个参数」判断是幂等的。
private func collectAnnotations(md: MacroExpandDecl): ArrayList<AnnotInfo> {
var result = ArrayList<AnnotInfo>()
result.add(AnnotInfo(md.identifier.value, md.macroAttrs.toString()))
let visitor = AnnotVisitor()
md.traverse(visitor)
for (a in visitor.getAnnotations()) {
result.add(a)
}
result
}
/// 注解列表中是否含指定注解名。
private func hasAnnotation(annots: ArrayList<AnnotInfo>, name: String): Bool {
for (an in annots) {
if (an.name == name) {
return true
}
}
false
}
/// 普通字段是否可序列化:有类型注解(不要求 _ 前缀)
private func isSerializableField(vd: VarDecl): Bool {
!vd.colon.value.isEmpty()
@@ -144,7 +198,20 @@ private class FieldVisitor <: Visitor {
}
}
/// 去掉字符串两端引号(SerializerPropertyName 属性 tokens 形如 "user_alias")
/// 遍历器:收集 MacroExpandDecl 子树内所有注解(用于识别 @PropertyName / @Ignore / @NotRequired)
private class AnnotVisitor <: Visitor {
private var _annotations = ArrayList<AnnotInfo>()
public override func visit(decl: MacroExpandDecl): Unit {
_annotations.add(AnnotInfo(decl.identifier.value, decl.macroAttrs.toString()))
}
public func getAnnotations(): ArrayList<AnnotInfo> {
_annotations
}
}
/// 去掉字符串两端引号(PropertyName 属性 tokens 形如 "user_alias")
private func stripQuotes(s: String): String {
if (s.size >= 2 && s[0..1] == "\"" && s[s.size - 1..] == "\"") {
return s[1..s.size - 1]
@@ -181,6 +248,14 @@ private func buildImportSource(className: String, fields: ArrayList<FieldInfo>):
sb.append(" }\n")
}
sb.append(" }\n")
// 默认必填:JSON 键必须存在,否则抛异常(@NotRequired 字段已置 required=false)
for (f in fields) {
if (f.isVar && f.required) {
sb.append(" if (!json.contains(\"${f.jsonName}\")) {\n")
sb.append(" throw Exception(\"JsonSerializer: 缺少必填字段 \\\"${f.jsonName}\\\"\")\n")
sb.append(" }\n")
}
}
sb.append(" }\n")
sb.toString()
}
+151 -63
View File
@@ -1,7 +1,7 @@
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* JsonSerializer 单元测试(cjpm test)。
* 覆盖:基础类型 / 对象字段输出 / 注解(SerializerPropertyName、SerializerIgnore)/ Option /
* 覆盖:基础类型 / 对象字段输出 / 注解(PropertyName、Ignore、NotRequired)/ Option /
* 集合(Array、ArrayList、HashSet)/ HashMap / 枚举 / 命名策略 /
* 继承(平台限制)/ maxDepth / 宽松类型转换。
*
@@ -10,12 +10,22 @@
package simcu::serialization.tests
import std.collection.*
import std.time.*
import std.collection.ArrayList
import std.collection.HashMap
import std.collection.HashSet
import std.time.DateTime
import std.unittest.*
import std.unittest.testmacro.*
import simcu::serialization.*
import simcu::serialization.macros.*
import std.unittest.testmacro.Expect
import std.unittest.testmacro.TestCase
import std.unittest.testmacro.Test
import simcu::serialization.JsonOption
import simcu::serialization.JsonSerializer
import simcu::serialization.PropertyNamingPolicy
import simcu::serialization.ReflectionCache
import simcu::serialization.Ignore
import simcu::serialization.NotRequired
import simcu::serialization.PropertyName
import simcu::serialization.macros.SerializerParent
enum Role {
| Admin
| Member
@@ -34,9 +44,9 @@ class Address {
open class BaseUser {
public var _id: String = ""
public var _createdAt: String = ""
@SerializerIgnore
@Ignore
public var _temp: String = ""
@SerializerPropertyName["parent_alias"]
@PropertyName["parent_alias"]
public var _parentAlias: String = ""
}
@@ -50,9 +60,9 @@ class User <: BaseUser {
public var remark: ?String = None // 无下划线(Option=None → null)
public var _tags: Array<String> = [] // 下划线字段(集合)
public var addr: Address = Address() // 无下划线(嵌套对象)
@SerializerPropertyName["user_alias"]
@PropertyName["user_alias"]
public var _alias: String = "" // 下划线 + 注解覆盖
@SerializerIgnore
@Ignore
public var temp: String = "secret" // 无下划线 + 忽略
}
@@ -63,6 +73,25 @@ class Event {
public var _optTime: ?DateTime = None
}
// 默认必填语义测试:默认所有字段必填;标 @NotRequired 的字段反序列化时允许缺失
class RequiredUser {
public var _id: String = "" // 默认必填:键缺失 → 抛异常
@NotRequired
public var _name: String = "" // @NotRequired:键缺失 → 允许,保持默认值
}
// 默认必填 + @SerializerParent:父类默认必填字段缺失时导入抛异常;@NotRequired 父类字段允许缺失
@SerializerParent
open class ReqBase {
public var _rid: String = "" // 默认必填
@NotRequired
public var _extra: String = "" // @NotRequired:允许缺失
}
class ReqChild <: ReqBase {
public var _own: String = "" // 默认必填
}
// 多层继承:爷(标宏)→ 父(不标宏)→ 子
@SerializerParent
open class GrandParent {
@@ -131,30 +160,30 @@ public class JsonSerializerTests {
/// 基础类型序列化
@TestCase
public func basicTypes(): Unit {
@Expect(JsonSerializer.Serialize(Int64(42)), "42")
@Expect(JsonSerializer.Serialize(Int32(7)), "7")
@Expect(JsonSerializer.Serialize(Int16(3)), "3")
@Expect(JsonSerializer.Serialize(UInt32(9)), "9")
@Expect(JsonSerializer.Serialize("hi"), "\"hi\"")
@Expect(JsonSerializer.Serialize(true), "true")
@Expect(JsonSerializer.Serialize(false), "false")
@Expect(JsonSerializer.Serialize(Float64(3.5)), "3.500000")
@Expect(JsonSerializer.serialize(Int64(42)), "42")
@Expect(JsonSerializer.serialize(Int32(7)), "7")
@Expect(JsonSerializer.serialize(Int16(3)), "3")
@Expect(JsonSerializer.serialize(UInt32(9)), "9")
@Expect(JsonSerializer.serialize("hi"), "\"hi\"")
@Expect(JsonSerializer.serialize(true), "true")
@Expect(JsonSerializer.serialize(false), "false")
@Expect(JsonSerializer.serialize(Float64(3.5)), "3.500000")
}
/// 对象序列化:下划线保留、注解生效、忽略生效、None→null、父类字段不输出
@TestCase
public func objectSerialize(): Unit {
let json = JsonSerializer.Serialize(makeUser())
let json = JsonSerializer.serialize(makeUser())
// 下划线字段原样输出
@Expect(json.contains("\"_name\": \"alice\""), true)
@Expect(json.contains("\"_age\": 30"), true)
// 无下划线字段原样输出
@Expect(json.contains("\"score\""), true)
@Expect(json.contains("\"active\": true"), true)
// @SerializerPropertyName 覆盖字段名
// @PropertyName 覆盖字段名
@Expect(json.contains("\"user_alias\": \"xiaoming\""), true)
@Expect(json.contains("\"_alias\""), false)
// @SerializerIgnore 字段不输出
// @Ignore 字段不输出
@Expect(json.contains("temp"), false)
@Expect(json.contains("\"secret\""), false)
// Option:Some→值,None→null
@@ -165,21 +194,21 @@ public class JsonSerializerTests {
// 嵌套对象
@Expect(json.contains("\"_city\": \"beijing\""), true)
@Expect(json.contains("\"zip\": \"100000\""), true)
// 父类字段(@SerializerParent 宏):_id/_createdAt 输出,@SerializerIgnore 父类字段不输出
// 父类字段(@SerializerParent 宏):_id/_createdAt 输出,@Ignore 父类字段不输出
@Expect(json.contains("\"_id\": \"u-1\""), true)
@Expect(json.contains("\"_createdAt\": \"2025-01-01\""), true)
// 父类 @SerializerPropertyName["parent_alias"] 生效
// 父类 @PropertyName["parent_alias"] 生效
@Expect(json.contains("\"parent_alias\": \"parent-xiaoming\""), true)
@Expect(json.contains("_parentAlias"), false)
// 父类 @SerializerIgnore _temp 不输出
// 父类 @Ignore _temp 不输出
@Expect(json.contains("secret-parent"), false)
}
/// 对象反序列化往返
@TestCase
public func objectRoundTrip(): Unit {
let json = JsonSerializer.Serialize(makeUser())
let u2 = JsonSerializer.Deserialize<User>(json)
let json = JsonSerializer.serialize(makeUser())
let u2 = JsonSerializer.deserialize<User>(json)
@Expect(u2._name, "alice")
@Expect(u2._age, Int64(30))
@Expect(u2.active, true)
@@ -187,7 +216,7 @@ public class JsonSerializerTests {
@Expect(u2.addr._city, "beijing")
@Expect(u2.addr.zip, "100000")
@Expect(u2._alias, "xiaoming")
@Expect(u2.temp, "secret") // @SerializerIgnore:反序列化不改动默认值
@Expect(u2.temp, "secret") // @Ignore:反序列化不改动默认值
// 父类字段往返(@SerializerParent 宏)
@Expect(u2._id, "u-1")
@Expect(u2._createdAt, "2025-01-01")
@@ -210,8 +239,8 @@ public class JsonSerializerTests {
/// Array 往返
@TestCase
public func arrayRoundTrip(): Unit {
let json = JsonSerializer.Serialize(["x", "y", "z"])
let arrBack = JsonSerializer.Deserialize<Array<String>>(json)
let json = JsonSerializer.serialize(["x", "y", "z"])
let arrBack = JsonSerializer.deserialize<Array<String>>(json)
@Expect(arrBack.size, Int64(3))
@Expect(arrBack[0], "x")
@Expect(arrBack[2], "z")
@@ -223,8 +252,8 @@ public class JsonSerializerTests {
var list = ArrayList<String>()
list.add("a")
list.add("b")
let json = JsonSerializer.Serialize(list)
let back = JsonSerializer.Deserialize<ArrayList<String>>(json)
let json = JsonSerializer.serialize(list)
let back = JsonSerializer.deserialize<ArrayList<String>>(json)
@Expect(back.size, Int64(2))
@Expect(back[0], "a")
@Expect(back[1], "b")
@@ -236,8 +265,8 @@ public class JsonSerializerTests {
var set = HashSet<String>()
set.add("x")
set.add("y")
let json = JsonSerializer.Serialize(set)
let back = JsonSerializer.Deserialize<HashSet<String>>(json)
let json = JsonSerializer.serialize(set)
let back = JsonSerializer.deserialize<HashSet<String>>(json)
@Expect(back.contains("x"), true)
@Expect(back.contains("y"), true)
}
@@ -248,8 +277,8 @@ public class JsonSerializerTests {
let m = HashMap<String, Any>()
m["k1"] = "v1"
m["k2"] = Int64(42)
let json = JsonSerializer.Serialize(m)
let mBack = JsonSerializer.Deserialize<HashMap<String, Any>>(json)
let json = JsonSerializer.serialize(m)
let mBack = JsonSerializer.deserialize<HashMap<String, Any>>(json)
@Expect(toStr(mBack["k1"]), "v1")
@Expect(toStr(mBack["k2"]), "42")
}
@@ -259,15 +288,15 @@ public class JsonSerializerTests {
public func namingPolicies(): Unit {
let optNone = JsonOption()
optNone.propertyNamingPolicy = PropertyNamingPolicy.Keep
let jsonNone = JsonSerializer.Serialize(makeUser(), optNone)
let jsonNone = JsonSerializer.serialize(makeUser(), optNone)
let optCamel = JsonOption()
optCamel.propertyNamingPolicy = PropertyNamingPolicy.CamelCase
let jsonCamel = JsonSerializer.Serialize(makeUser(), optCamel)
let jsonCamel = JsonSerializer.serialize(makeUser(), optCamel)
let optSnake = JsonOption()
optSnake.propertyNamingPolicy = PropertyNamingPolicy.SnakeCase
let jsonSnake = JsonSerializer.Serialize(makeUser(), optSnake)
let jsonSnake = JsonSerializer.serialize(makeUser(), optSnake)
// 模型字段全为小写/下划线,三种策略结果应一致
@Expect(jsonNone, jsonCamel)
@@ -292,20 +321,20 @@ public class JsonSerializerTests {
/// 枚举序列化/反序列化
@TestCase
public func enumSerialize(): Unit {
@Expect(JsonSerializer.Serialize(Role.Admin), "\"Admin\"")
@Expect(JsonSerializer.Serialize(Role.Guest), "\"Guest\"")
@Expect(JsonSerializer.serialize(Role.Admin), "\"Admin\"")
@Expect(JsonSerializer.serialize(Role.Guest), "\"Guest\"")
let u = User()
u._role = Role.Guest
let back = JsonSerializer.Deserialize<User>(JsonSerializer.Serialize(u))
let back = JsonSerializer.deserialize<User>(JsonSerializer.serialize(u))
@Expect(roleStr(back._role), "Guest")
}
/// Option 字段:null → None,值 → Some
@TestCase
public func optionField(): Unit {
let none: ?String = JsonSerializer.Deserialize<?String>("null")
let none: ?String = JsonSerializer.deserialize<?String>("null")
@Expect(none.isNone(), true)
let some: ?String = JsonSerializer.Deserialize<?String>("\"hi\"")
let some: ?String = JsonSerializer.deserialize<?String>("\"hi\"")
@Expect(some.isSome(), true)
if (let Some(v) <- some) {
@Expect(v, "hi")
@@ -319,10 +348,10 @@ public class JsonSerializerTests {
e._name = "login"
e._time = DateTime.parse("2026-08-21 02:27:48", "yyyy-MM-dd HH:mm:ss")
e._optTime = Some(DateTime.parse("2026-01-02 03:04:05", "yyyy-MM-dd HH:mm:ss"))
let json = JsonSerializer.Serialize(e)
let json = JsonSerializer.serialize(e)
@Expect(json.contains("\"_time\": \"2026-08-21 02:27:48\""), true)
@Expect(json.contains("\"_optTime\": \"2026-01-02 03:04:05\""), true)
let back = JsonSerializer.Deserialize<Event>(json)
let back = JsonSerializer.deserialize<Event>(json)
@Expect(back._time.format("yyyy-MM-dd HH:mm:ss"), "2026-08-21 02:27:48")
let optVal: ?DateTime = back._optTime
@Expect(optVal.isSome(), true)
@@ -330,14 +359,14 @@ public class JsonSerializerTests {
@Expect(dt.format("yyyy-MM-dd HH:mm:ss"), "2026-01-02 03:04:05")
}
// 顶层 DateTime 直接序列化
@Expect(JsonSerializer.Serialize(back._time), "\"2026-08-21 02:27:48\"")
@Expect(JsonSerializer.serialize(back._time), "\"2026-08-21 02:27:48\"")
// Option<DateTime> None → null
let e2 = Event()
e2._name = "n"
e2._time = DateTime.parse("2026-01-01 00:00:00", "yyyy-MM-dd HH:mm:ss")
let json2 = JsonSerializer.Serialize(e2)
let json2 = JsonSerializer.serialize(e2)
@Expect(json2.contains("\"_optTime\": null"), true)
let back2 = JsonSerializer.Deserialize<Event>(json2)
let back2 = JsonSerializer.deserialize<Event>(json2)
@Expect(back2._optTime.isNone(), true)
}
@@ -345,16 +374,16 @@ public class JsonSerializerTests {
@TestCase
public func looseConversion(): Unit {
// 数字 → String 字段
let s: String = JsonSerializer.Deserialize<String>("42")
let s: String = JsonSerializer.deserialize<String>("42")
@Expect(s, "42")
// 字符串 → Int64 字段
let i: Int64 = JsonSerializer.Deserialize<Int64>("\"42\"")
let i: Int64 = JsonSerializer.deserialize<Int64>("\"42\"")
@Expect(i, Int64(42))
// 浮点 → Int64(截断)
let j: Int64 = JsonSerializer.Deserialize<Int64>("3.9")
let j: Int64 = JsonSerializer.deserialize<Int64>("3.9")
@Expect(j, Int64(3))
// 字符串 → Bool
let b: Bool = JsonSerializer.Deserialize<Bool>("\"true\"")
let b: Bool = JsonSerializer.deserialize<Bool>("\"true\"")
@Expect(b, true)
}
@@ -365,7 +394,7 @@ public class JsonSerializerTests {
opt.maxDepth = 0
let u = User()
let threw = try {
JsonSerializer.Serialize(u, opt)
JsonSerializer.serialize(u, opt)
false
} catch (_: Exception) {
true
@@ -386,16 +415,75 @@ public class JsonSerializerTests {
@Expect(toStr(m["_id"]), "m-1")
@Expect(toStr(m["_createdAt"]), "2025-02-02")
@Expect(toStr(m["parent_alias"]), "alias-m")
// @SerializerIgnore 父类字段不导出
// @Ignore 父类字段不导出
@Expect(m.contains("_temp"), false)
// 宏生成的导入方法
// 宏生成的导入方法(默认必填:三个 var 字段键都须存在,@Ignore 的 _temp 除外)
var json = HashMap<String, Any>()
json["_id"] = "m-2"
json["_createdAt"] = "2025-03-03"
json["parent_alias"] = "alias-m2"
BaseUser.importJsonFields(u, json)
@Expect(u._id, "m-2")
@Expect(u._parentAlias, "alias-m2")
@Expect(u._createdAt, "2025-02-02") // 未传的键保持原值
@Expect(u._createdAt, "2025-03-03") // 键存在则回填
}
/// 默认必填:未标注字段键缺失 → 抛异常;@NotRequired 字段键缺失 → 允许(保持默认值)
@TestCase
public func requiredField(): Unit {
// 全部键存在:正常反序列化
let ok = JsonSerializer.deserialize<RequiredUser>("{\"_id\": \"r-1\", \"_name\": \"tom\"}")
@Expect(ok._id, "r-1")
@Expect(ok._name, "tom")
// 默认必填字段 _id 缺失 → 抛异常(即便 @NotRequired 的 _name 存在)
let threw = try {
JsonSerializer.deserialize<RequiredUser>("{\"_name\": \"tom\"}")
false
} catch (_: Exception) {
true
}
@Expect(threw, true)
// @NotRequired 字段 _name 缺失 → 允许,保持默认值
let okMissing = JsonSerializer.deserialize<RequiredUser>("{\"_id\": \"r-2\"}")
@Expect(okMissing._id, "r-2")
@Expect(okMissing._name, "")
// 默认必填字段键存在但为 null:视为「存在」,不抛异常,保持默认值
let nullId = try {
let u = JsonSerializer.deserialize<RequiredUser>("{\"_id\": null, \"_name\": \"tom\"}")
u._id
} catch (_: Exception) {
"<threw>"
}
@Expect(nullId, "")
}
/// 默认必填 + @SerializerParent:父类默认必填字段缺失 → 上抛;@NotRequired 父类字段缺失 → 允许
@TestCase
public func requiredParentField(): Unit {
// 全部键存在:正常反序列化
let ok = try {
let c = JsonSerializer.deserialize<ReqChild>("{\"_rid\": \"r-1\", \"_own\": \"x\"}")
c._rid
} catch (_: Exception) {
"<threw>"
}
@Expect(ok, "r-1")
// 父类默认必填字段 _rid 缺失 → importJsonFields 抛"缺少必填字段"并上抛
let threw = try {
JsonSerializer.deserialize<ReqChild>("{\"_extra\": \"e\", \"_own\": \"x\"}")
false
} catch (_: Exception) {
true
}
@Expect(threw, true)
// 父类 @NotRequired 字段 _extra 缺失 → 允许;但子类默认必填字段 _own 须存在
let okMissing = try {
let c = JsonSerializer.deserialize<ReqChild>("{\"_rid\": \"r-2\", \"_own\": \"x\"}")
c._extra
} catch (_: Exception) {
"<threw>"
}
@Expect(okMissing, "")
}
/// 多层继承:爷标宏、中间层不标宏 → 爷字段导出,中间层字段不导出
@@ -406,12 +494,12 @@ public class JsonSerializerTests {
leaf._mp = "mp-v"
leaf._lc = "lc-v"
let json = JsonSerializer.Serialize(leaf)
let json = JsonSerializer.serialize(leaf)
@Expect(json.contains("\"_gp\": \"gp-v\""), true) // 爷字段(宏)导出
@Expect(json.contains("_mp"), false) // 中间层无宏不导出
@Expect(json.contains("\"_lc\": \"lc-v\""), true) // 子字段(反射)导出
let back = JsonSerializer.Deserialize<LeafChild>(json)
let back = JsonSerializer.deserialize<LeafChild>(json)
@Expect(back._gp, "gp-v")
@Expect(back._lc, "lc-v")
@Expect(back._mp, "") // 中间层字段保持默认值
@@ -422,7 +510,7 @@ public class JsonSerializerTests {
public func parentAutoOpen(): Unit {
let leaf = LeafChild()
leaf._gp = "auto"
let json = JsonSerializer.Serialize(leaf)
let json = JsonSerializer.serialize(leaf)
@Expect(json.contains("\"_gp\": \"auto\""), true)
}
@@ -434,12 +522,12 @@ public class JsonSerializerTests {
leaf._ma = "ma-v"
leaf._la = "la-v"
let json = JsonSerializer.Serialize(leaf)
let json = JsonSerializer.serialize(leaf)
@Expect(json.contains("\"_ga\": \"ga-v\""), true) // 爷字段(宏)导出
@Expect(json.contains("\"_ma\": \"ma-v\""), true) // 父字段(宏)导出
@Expect(json.contains("\"_la\": \"la-v\""), true) // 子字段(反射)导出
let back = JsonSerializer.Deserialize<LeafAll>(json)
let back = JsonSerializer.deserialize<LeafAll>(json)
@Expect(back._ga, "ga-v")
@Expect(back._ma, "ma-v")
@Expect(back._la, "la-v")
@@ -452,7 +540,7 @@ public class JsonSerializerTests {
leaf._ga = "G"
leaf._ma = "M"
leaf._la = "L"
let json = JsonSerializer.Serialize(leaf)
let json = JsonSerializer.serialize(leaf)
// 三个字段值互不串
@Expect(json.contains("\"_ga\": \"G\""), true)
@@ -460,7 +548,7 @@ public class JsonSerializerTests {
@Expect(json.contains("\"_la\": \"L\""), true)
// 反序列化后互不覆盖
let back = JsonSerializer.Deserialize<LeafAll>(json)
let back = JsonSerializer.deserialize<LeafAll>(json)
@Expect(back._ga, "G")
@Expect(back._ma, "M")
@Expect(back._la, "L")