diff --git a/README.md b/README.md index e154d25..beb99e7 100644 --- a/README.md +++ b/README.md @@ -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(json, JsonOption)` +- **API 对齐**:`JsonSerializer.serialize(obj, JsonOption)` / `deserialize(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(json) +let u2 = JsonSerializer.deserialize(json) // 全部字段回填 ``` @@ -64,14 +65,52 @@ let u2 = JsonSerializer.Deserialize(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(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() -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("42")` → `"42"`, -`Deserialize("\"42\"")` → `42`。 +JSON 数字 ↔ 字符串字段自动互转:`deserialize("42")` → `"42"`, +`deserialize("\"42\"")` → `42`。 ## 已知限制 @@ -190,24 +229,24 @@ JSON 数字 ↔ 字符串字段自动互转:`Deserialize("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`) diff --git a/cjpm.toml b/cjpm.toml index bbb0528..2196452 100644 --- a/cjpm.toml +++ b/cjpm.toml @@ -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" diff --git a/docs/父类字段序列化方案.md b/docs/父类字段序列化方案.md index 46a490e..41b3b91 100644 --- a/docs/父类字段序列化方案.md +++ b/docs/父类字段序列化方案.md @@ -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() 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` 后父类字段值正确 | | 多层继承 | 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 编译失败 | diff --git a/src/SimApiSerialization.cj b/src/SimApiSerialization.cj index 9569343..c11b07b 100644 --- a/src/SimApiSerialization.cj +++ b/src/SimApiSerialization.cj @@ -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 diff --git a/src/common/Annotations.cj b/src/common/Annotations.cj index d7d17c4..c212582 100644 --- a/src/common/Annotations.cj +++ b/src/common/Annotations.cj @@ -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() {} } diff --git a/src/common/ReflectionCache.cj b/src/common/ReflectionCache.cj index 3d6d2c6..7dab52b 100644 --- a/src/common/ReflectionCache.cj +++ b/src/common/ReflectionCache.cj @@ -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>() + private static let _CACHE = HashMap>() /// 类型 → 是否有 @SerializerParent 宏生成的 exportJsonFields(Bool) - private static let _exportCache = HashMap() + private static let _EXPORT_CACHE = HashMap() /// 类型 → 是否有 @SerializerParent 宏生成的 importJsonFields(Bool) - private static let _importCache = HashMap() + private static let _IMPORT_CACHE = HashMap() /** * 该类型是否含 @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): Bool { @@ -84,7 +89,7 @@ public class ReflectionCache { */ public static func getFields(typeInfo: ClassTypeInfo, namingPolicy: PropertyNamingPolicy): ArrayList { let key = typeInfo.qualifiedName - if (let Some(cached) <- _cache.get(key)) { + if (let Some(cached) <- _CACHE.get(key)) { return cached } var fields = ArrayList() @@ -94,13 +99,17 @@ public class ReflectionCache { meta.variable = Some(v) meta.typeInfo = Some(v.typeInfo) meta.mutable = v.isMutable() - // @SerializerIgnore - if (v.findAnnotation().isSome()) { + // @Ignore + if (v.findAnnotation().isSome()) { meta.ignore = true } - // @SerializerPropertyName 优先 + // @NotRequired:反序列化时允许该字段缺失(默认所有字段必填) + if (v.findAnnotation().isSome()) { + meta.required = false + } + // @PropertyName 优先 var jsonName = "" - if (let Some(jsonProp) <- v.findAnnotation()) { + if (let Some(jsonProp) <- v.findAnnotation()) { 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() } diff --git a/src/json/JsonOption.cj b/src/json/JsonOption.cj index 246b122..808c7f9 100644 --- a/src/json/JsonOption.cj +++ b/src/json/JsonOption.cj @@ -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() } diff --git a/src/json/JsonReader.cj b/src/json/JsonReader.cj index 7e6a42f..04e5d78 100644 --- a/src/json/JsonReader.cj +++ b/src/json/JsonReader.cj @@ -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(value: JsonValue, options: JsonOption): T { let result = readValue(value, TypeInfo.of(), 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>()]) var pjson = HashMap() - 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 = [instance, pjson] fn.apply(p, args) - } catch (_: Exception) { - // 导入失败(如类型不匹配)不中断整体反序列化 + } catch (e: Exception) { + // 默认必填字段缺失 → 中断整体反序列化 + if (e.message.contains("缺少必填字段")) { + throw e + } + // 其余导入失败(如类型不匹配)不中断整体反序列化 + let _ = e } } parent = p.superClass } } - /// Array:经 ArrayList 构造 + add + toArray + /// Array:经 ArrayList 构造 + 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。 + /// @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。 + /// @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。 + /// @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() - for (item in v.asArray().getItems()) { - list.add(jsonToAny(item)) - } - list.toArray() - case JsObject => - var map = HashMap() - 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() + for (item in a.getItems()) { + list.add(jsonToAny(item)) + } + return list.toArray() } + if (let o: JsonObject <- v) { + var map = HashMap() + 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 的 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}") } diff --git a/src/json/JsonSerializer.cj b/src/json/JsonSerializer.cj index be3cf95..d821e74 100644 --- a/src/json/JsonSerializer.cj +++ b/src/json/JsonSerializer.cj @@ -1,19 +1,20 @@ /* * Copyright (c) 2025 SimcuTeam. All rights reserved. - * JsonSerializer:公开 API,对齐 .NET System.Text.Json: - * JsonSerializer.Serialize(obj, JsonOption) → JSON 字符串 - * JsonSerializer.Deserialize(jsonString, JsonOption) → T + * JsonSerializer:公开 API: + * serialize(obj, JsonOption) → JSON 字符串 + * deserialize(jsonString, JsonOption) → T + * deserialize(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(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(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(jsonString: String, options: JsonOption): T { + public static func deserialize(jsonString: String, options: JsonOption): T { let value = JsonValue.fromStr(jsonString) JsonReader.read(value, options) } /** * 反序列化 JSON 字符串为目标类型(默认选项)。 + * @param jsonString JSON 字符串。 + * @return T 实例(要求 T 有无参构造,字段为 var)。 + * @throws Exception JSON 格式非法、类型不匹配或目标类型不支持时抛出。 */ - public static func Deserialize(jsonString: String): T { - Deserialize(jsonString, JsonOption.instance) + public static func deserialize(jsonString: String): T { + deserialize(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(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(typeInfo: TypeInfo, jsonString: String): T { + deserialize(typeInfo, jsonString, JsonOption.INSTANCE) } } diff --git a/src/json/JsonWriter.cj b/src/json/JsonWriter.cj index 09aefb7..f3845f2 100644 --- a/src/json/JsonWriter.cj +++ b/src/json/JsonWriter.cj @@ -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:Some(元素) / None(越界结束) let getFunc = arrTi.getInstanceFunction("get", [TypeInfo.of()]) + 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" / "std.collection.HashMap" / "Option" 提取类型参数 public static func extractTypeArgs(typeName: String): ArrayList { - var args = ArrayList() + let args = ArrayList() 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): 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) + } + } } diff --git a/src/macros/SerializerParentMacro.cj b/src/macros/SerializerParentMacro.cj index 09828a5..7017d08 100644 --- a/src/macros/SerializerParentMacro.cj +++ b/src/macros/SerializerParentMacro.cj @@ -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 * - 生成 importJsonFields(instance: T, json: HashMap): 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 { var result = ArrayList() for (d in cd.body.decls) { @@ -96,20 +121,25 @@ private func collectFields(cd: ClassDecl): ArrayList { 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 { result } +/// 收集字段上全部注解(含嵌套,支持 @NotRequired 与 @PropertyName 组合)。 +/// 手动记录最外层注解后再遍历内部,避免遍历器是否含根节点的不确定性; +/// 重复收集同名注解对后续「是否存在」与「取哪个参数」判断是幂等的。 +private func collectAnnotations(md: MacroExpandDecl): ArrayList { + var result = ArrayList() + 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, 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() + + public override func visit(decl: MacroExpandDecl): Unit { + _annotations.add(AnnotInfo(decl.identifier.value, decl.macroAttrs.toString())) + } + + public func getAnnotations(): ArrayList { + _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): 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() } diff --git a/src/tests/JsonSerializer_test.cj b/src/tests/JsonSerializer_test.cj index 51f0467..ddd1736 100644 --- a/src/tests/JsonSerializer_test.cj +++ b/src/tests/JsonSerializer_test.cj @@ -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 = [] // 下划线字段(集合) 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(json) + let json = JsonSerializer.serialize(makeUser()) + let u2 = JsonSerializer.deserialize(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>(json) + let json = JsonSerializer.serialize(["x", "y", "z"]) + let arrBack = JsonSerializer.deserialize>(json) @Expect(arrBack.size, Int64(3)) @Expect(arrBack[0], "x") @Expect(arrBack[2], "z") @@ -223,8 +252,8 @@ public class JsonSerializerTests { var list = ArrayList() list.add("a") list.add("b") - let json = JsonSerializer.Serialize(list) - let back = JsonSerializer.Deserialize>(json) + let json = JsonSerializer.serialize(list) + let back = JsonSerializer.deserialize>(json) @Expect(back.size, Int64(2)) @Expect(back[0], "a") @Expect(back[1], "b") @@ -236,8 +265,8 @@ public class JsonSerializerTests { var set = HashSet() set.add("x") set.add("y") - let json = JsonSerializer.Serialize(set) - let back = JsonSerializer.Deserialize>(json) + let json = JsonSerializer.serialize(set) + let back = JsonSerializer.deserialize>(json) @Expect(back.contains("x"), true) @Expect(back.contains("y"), true) } @@ -248,8 +277,8 @@ public class JsonSerializerTests { let m = HashMap() m["k1"] = "v1" m["k2"] = Int64(42) - let json = JsonSerializer.Serialize(m) - let mBack = JsonSerializer.Deserialize>(json) + let json = JsonSerializer.serialize(m) + let mBack = JsonSerializer.deserialize>(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(JsonSerializer.Serialize(u)) + let back = JsonSerializer.deserialize(JsonSerializer.serialize(u)) @Expect(roleStr(back._role), "Guest") } /// Option 字段:null → None,值 → Some @TestCase public func optionField(): Unit { - let none: ?String = JsonSerializer.Deserialize("null") + let none: ?String = JsonSerializer.deserialize("null") @Expect(none.isNone(), true) - let some: ?String = JsonSerializer.Deserialize("\"hi\"") + let some: ?String = JsonSerializer.deserialize("\"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(json) + let back = JsonSerializer.deserialize(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 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(json2) + let back2 = JsonSerializer.deserialize(json2) @Expect(back2._optTime.isNone(), true) } @@ -345,16 +374,16 @@ public class JsonSerializerTests { @TestCase public func looseConversion(): Unit { // 数字 → String 字段 - let s: String = JsonSerializer.Deserialize("42") + let s: String = JsonSerializer.deserialize("42") @Expect(s, "42") // 字符串 → Int64 字段 - let i: Int64 = JsonSerializer.Deserialize("\"42\"") + let i: Int64 = JsonSerializer.deserialize("\"42\"") @Expect(i, Int64(42)) // 浮点 → Int64(截断) - let j: Int64 = JsonSerializer.Deserialize("3.9") + let j: Int64 = JsonSerializer.deserialize("3.9") @Expect(j, Int64(3)) // 字符串 → Bool - let b: Bool = JsonSerializer.Deserialize("\"true\"") + let b: Bool = JsonSerializer.deserialize("\"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() 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("{\"_id\": \"r-1\", \"_name\": \"tom\"}") + @Expect(ok._id, "r-1") + @Expect(ok._name, "tom") + // 默认必填字段 _id 缺失 → 抛异常(即便 @NotRequired 的 _name 存在) + let threw = try { + JsonSerializer.deserialize("{\"_name\": \"tom\"}") + false + } catch (_: Exception) { + true + } + @Expect(threw, true) + // @NotRequired 字段 _name 缺失 → 允许,保持默认值 + let okMissing = JsonSerializer.deserialize("{\"_id\": \"r-2\"}") + @Expect(okMissing._id, "r-2") + @Expect(okMissing._name, "") + // 默认必填字段键存在但为 null:视为「存在」,不抛异常,保持默认值 + let nullId = try { + let u = JsonSerializer.deserialize("{\"_id\": null, \"_name\": \"tom\"}") + u._id + } catch (_: Exception) { + "" + } + @Expect(nullId, "") + } + + /// 默认必填 + @SerializerParent:父类默认必填字段缺失 → 上抛;@NotRequired 父类字段缺失 → 允许 + @TestCase + public func requiredParentField(): Unit { + // 全部键存在:正常反序列化 + let ok = try { + let c = JsonSerializer.deserialize("{\"_rid\": \"r-1\", \"_own\": \"x\"}") + c._rid + } catch (_: Exception) { + "" + } + @Expect(ok, "r-1") + // 父类默认必填字段 _rid 缺失 → importJsonFields 抛"缺少必填字段"并上抛 + let threw = try { + JsonSerializer.deserialize("{\"_extra\": \"e\", \"_own\": \"x\"}") + false + } catch (_: Exception) { + true + } + @Expect(threw, true) + // 父类 @NotRequired 字段 _extra 缺失 → 允许;但子类默认必填字段 _own 须存在 + let okMissing = try { + let c = JsonSerializer.deserialize("{\"_rid\": \"r-2\", \"_own\": \"x\"}") + c._extra + } catch (_: Exception) { + "" + } + @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(json) + let back = JsonSerializer.deserialize(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(json) + let back = JsonSerializer.deserialize(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(json) + let back = JsonSerializer.deserialize(json) @Expect(back._ga, "G") @Expect(back._ma, "M") @Expect(back._la, "L")