Teyru

JSON 与 Gson 兼容层

Gson 形状的树状 API,以及运行期读取类字段的对象绑定。

Teyru 的 JSON 分成两层:lib/10_json.teyru 是 Gson 的树状 API(纯 Teyru,不需要 编译器配合),lib/19_gson.teyru 加上编译器的对象绑定。

树状 API(lib/10)

JsonElement、JsonObject、JsonArray、JsonPrimitive、JsonNull、 JsonParser.parseString、Gson、GsonBuilder、JsonSyntaxException, 行为对齐 Gson 2.10,包含几个容易做错的细节:

  • JsonNull 继承 JsonElement 而不是 JsonPrimitive。
  • 解析后的数字保留原始字面量,所以 1e5 打印回来还是 1e5,不会变成 100000.0。
  • 转义规则照 Gson 的替换表:" 与 \ 会转义、五个短形式 \b \t \n \f \r、 其余小于 0x20 的用 \u00xx(小写十六进制)、/ 不转义、非 ASCII 以 UTF-8 输出而不是 \u 转义,只有 U+2028/U+2029 例外;<、>、&、=、' 只在 htmlSafe 的写法(默认的 new Gson())下转义,JsonElement.toString() 不转义。
  • 空对象与空数组在 pretty print 时保持一行({}、[])。

与 Gson 的差异(刻意的):

  • JsonParser.parseString 是严格的(RFC 8259)。Gson 宽松模式接受的 {a:1}、 '单引号'、01、+1、.5、1.、NaN、[1,] 一律拒绝并抛出 JsonSyntaxException;空文档也拒绝,尾端多余数据一律拒绝。
  • entrySet() 返回 List<JsonMember>、keySet() 返回 List<String>(Gson 给的是 Set);JsonMember 的 getKey/getValue 对应 Map.Entry。
  • 数字只到 long,没有 getAsBigDecimal/getAsBigInteger。
  • 数值访问器抛出 IllegalArgumentException 而不是 NumberFormatException (前者是后者的父类,所以 catch 父类的代码不受影响;指名 NumberFormatException 的 catch 拦不到它们)。

对象绑定(lib/19 加上编译器)

Gson 用反射把对象绑定到 JSON。这里编译器代劳:

import teyru.*

class Address {
  String city
  String zip
}

class Person {
  String name
  int age
  @SerializedName("home_address") Address address
}

Gson gson = new Gson()
Person p = gson.fromJson(json, Person.class)   // 不需要 cast
String out = gson.toJson(p)

编译器看到 fromJson(s, Person.class) 就在 Person 上生成 __teyruJsonRead(JsonElement) 与 __teyruJsonWrite(),并把调用改写成它们。因此:

  • 不需要 cast:Gson 的 <T> T fromJson(String, Class<T>) 在 Teyru 无法表达 (Class 不是泛型),但改写后的返回类型就是目标类本身。
  • 字段类型没有映射是编译错误(TY-TYP-0110;List/Map 这类 prelude 类是 TY-TYP-0108),而不是运行时从反射适配器深处抛出的 IllegalArgumentException。
  • 绑定是按调用点生成的:没有用到就不生成,而且字段列表是生成当时的(Lombok 生成的成员也在内)。

支持的字段类型:String、boolean/byte/short/char/int/long/float/ double 与其装箱类型、enum(写成常量名,读回来比对名字,与 Gson 相同;名字对不上 时 Gson 留 null,这里抛 JsonParseException)、其他可绑定的类(递归)、 Object(保留原始树:读进来的 JsonObject 原样写回去),以及容器——List/Set/ Map/Deque 等接口与其实、数组,容器里的对象元素也绑定(元素类型由编译器写进 字段描述符,Field.getElementType() 读得到;反射本身仍然没有泛型类型)。char 和 Gson 一样写成单字符字符串、也从字符串读回来。

GsonBuilder 的旋钮(serializeNulls/setPrettyPrinting/setLenient/ setFieldNamingPolicy 的五种策略/excludeFieldsWithModifiers/ excludeFieldsWithoutExposeAnnotation/setVersion)、@Expose/@Since/@Until/ @SerializedName 的过滤与改名、JsonSerializer/JsonDeserializer 适配器 (registerTypeAdapter/registerTypeHierarchyAdapter/@JsonAdapter)与流式 JsonReader/JsonWriter 都在(t166_gson_builder、t167_gson_annotations、 t168_gson_adapters;容器是 t157_json_collections、t165_json_container_elements)。

仍然不支持的:元素类型只记一层(List<List<Person>> 的内层元素按文档的形状判读, 里面是对象或数组就明确拒绝;声明为 Object 的元素也一样)、数组的 component 是 ? super 或未定界类型变量时没有类可以建数组(该成员明确拒绝)、 @SerializedName 的 alternate(多个名字要用重复标注写,第一个是写出用的名字, 其余是读入也接受的替代名)、@JsonAdapter 用字符串命名适配器类。

运行时回退表

静态类型是 Object 的引用(gson.toJson(someObject))编译器看不穿,这时走 JsonBinding 这张表:每个生成过绑定的类在自己的静态初始化里注册reader/ writer,查表用对象自己的类——这正是 Gson 反射给出的答案。

Object 可以装着程序里的任何类,所以这种调用点一出现,编译器就替程序中声明的 每个类都生成一份绑定(接口、抽象类、没有无参构造函数又无法从 JSON 读取的 类除外;那些生成不出来的就不登记,运行时问到会说没有绑定)。没有这个调用点的 程序仍然只为真正用到的类生成绑定。

相关测试

  • tests/programs/t93_json.teyru — 树状 API:转义、数字、pretty print、往返。
  • tests/programs/t101_gson.teyru — 对象绑定:嵌套类、@SerializedName、 缺字段、运行时回退表。
  • tests/programs/t140_json_binding_edges.teyru — 边界:char 往返、Object 字段里的树、只通过 Object 传递的类、enum 与未知的常量名称。

本页目录