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 與未知的常數名稱。