此 EEP 提議將一個 json
模組引入 Erlang 標準函式庫,支援從 Erlang 資料結構編碼和解碼 JSON 文件。主要原因是要彌補 Erlang 標準函式庫在這種廣泛流行且普遍使用的資料格式方面的不足。
JSON 在許多不同的使用案例中很常見
目前有許多 Erlang 和其他 BEAM 語言的 JSON 函式庫,但是將此支援新增到標準函式庫將會帶來獨特的好處。最顯著的是能够在無法輕易使用第三方函式庫的情況下使用它,例如獨立的 escript 或基本工具(如建置系統),或在 OTP 本身內使用。
先前曾嘗試將 JSON 支援引入 OTP,最著名的是 EEP 18,但由於各種原因最終未被採用。但是,我相信現在是時候以新的角度重新檢視這個主題,並考慮這種支援可能採用的介面。
JSON 是在 RFC 8259 和 ECMA 404 中平行規範的完善格式,但是由於資料結構沒有直接的 1:1 對應關係,因此如何將此表示法轉換為 Erlang 並不完全清楚。為了協助處理此問題,此 EEP 提出了一個介面,該介面提供了一個方便且「規範」的簡單 API,以及一個具有常見底層實作的可擴展且高度可自訂的 API。
此 EEP 提出了一個 JSON 函式庫,該函式庫
提出的 JSON 函式庫將提供
我們建議在「標準」API 中,以以下方式將 JSON 資料結構對應到 Erlang 和從 Erlang 對應回 JSON
從 JSON 解碼 | Erlang | 編碼為 JSON |
---|---|---|
數字 | integer() | float() | 數字 |
布林值 | true | false | 布林值 |
空值 | null | 空值 |
字串 | binary() | 字串 |
atom() | 字串 | |
陣列 | list() | 陣列 |
物件 | #{binary() => _} | 物件 |
#{atom() => _} | 物件 | |
#{integer() => _} | 物件 |
Erlang 通常比 JSON 具有更豐富的值系統,因此通常有更多類型可以編碼為 JSON,即使它們永遠無法由解碼器直接產生。
但是,使用如下所示的彈性 API,使用者將能夠自訂解碼和編碼常式,以便在特定應用程式中根據需要產生和使用任何 Erlang 項。
注意:即使使用自訂解碼器,解碼-編碼往返也可能不會產生相同的資料——因為與 Erlang 相比,JSON 的資料類型選項非常有限,因此某些資訊通常會遺失,例如,強制將映射中的所有鍵轉換為二進位。
在資料結構剖析器方面,通常會遇到兩種類型:一種是給定資料產生完整的剖析值,另一種是相同的資料產生事件串流,這些事件稍後可以處理以提取值。
第一種,我們在此處稱之為基於值的剖析器,通常更簡單、效率更高且更方便使用。第二種在特定使用案例中提供了獨特的優勢:例如,資料無法完全放入記憶體中。
對於建議的 json
函式庫,此 EEP 建議採用混合方法。
首先,一個簡單的基於值的 API
-type value() ::
integer() |
float() |
boolean() |
null |
binary() |
list(value()) |
#{binary() => value()}.
-spec decode(binary()) -> value().
錯誤處理透過例外處理實現。可能發生以下錯誤
-type error() ::
unexpected_end |
{unexpected_sequence, binary()} |
{invalid_byte, byte()}
例外處理可能會透過 錯誤資訊 機制增強,其中包含額外的中繼資料,例如發生錯誤的位元組偏移量。
對於進階且可自訂的 API,此 EEP 提出一個基於回呼的 API,解碼器將使用該 API 從其剖析的資料中產生值。
-type from_binary_fun() :: fun((binary()) -> dynamic()).
-type array_start_fun() :: fun((Acc :: dynamic()) -> ArrayAcc :: dynamic()).
-type array_push_fun() :: fun((Value :: dynamic(), Acc :: dynamic()) -> NewAcc :: dynamic()).
-type array_finish_fun() :: fun((ArrayAcc :: dynamic(), OldAcc :: dynamic()) -> {dynamic(), Acc :: dynamic()}).
-type object_start_fun() :: fun((Acc :: dynamic()) -> ObjectAcc :: dynamic()).
-type object_push_fun() :: fun((Key :: dynamic(), Value :: dynamic(), Acc :: dynamic()) -> NewAcc :: dynamic()).
-type object_finish_fun() :: fun((ObjectAcc :: dynamic(), OldAcc :: dynamic()) -> {dynamic(), Acc :: dynamic()}).
-type decoders() :: #{
array_start => array_start_fun(),
array_push => array_push_fun(),
array_finish => array_finish_fun(),
object_start => object_start_fun(),
object_push => object_push_fun(),
object_finish => object_finish_fun(),
float => from_binary_fun(),
integer => from_binary_fun(),
string => from_binary_fun(),
null => term()
}.
-spec decode(binary(), Acc :: dynamic(), decoders()) ->
{Value :: dynamic(), FinalAcc :: dynamic(), Rest :: binary()}.
這允許使用者完全自訂解碼格式,包括在開放原始碼 JSON 函式庫中看到的功能
null
解碼為另一個 atom,特別是 undefined
或 nil
;binary:copy/1
;此外,這允許使用者僅保留部分資料結構,以實現類似於對無法完全放入記憶體的資料使用串流 SAX 剖析器的結果。
array_finish
和 object_finish
回呼負責還原累加器以繼續處理父物件。為了簡化累加器未連接的情況,這些回呼接收傳遞給相應 _start
呼叫的累加器值。
所有回呼都是可選的,並且具有對應於「簡單」API 行為的預設值,使用列表作為累加器,特別是
array_start
:fun(_) -> [] end
array_push
:fun(Elem, Acc) -> [Elem | Acc] end
array_finish
:fun(Acc, OldAcc) -> {lists:reverse(Acc), OldAcc} end
object_start
:fun(_) -> [] end
object_push
:fun(Key, Value, Acc) -> [{Key, Value} | Acc] end
object_finish
:fun(Acc, OldAcc) -> {maps:from_list(Acc), OldAcc} end
float
:fun erlang:binary_to_float/1
integer
:fun erlang:binary_to_integer/1
string
:fun (Value) -> Value end
null
:atom null
我們建議將來增強完整 decode/3
API,其中它可以返回 {incomplete, continuation()}
值,可用於解碼跨多個二進位 Blob 分割的值(例如從 TCP Socket 接收到的值)。
-spec decode_continue(binary(), continuation()) ->
{Value :: dynamic(), FinalAcc :: dynamic(), Rest :: binary()} |
{incomplete, continuation()}.
對於編碼,此 EEP 再次提出兩組獨立的 API。使用「標準」資料類型的簡單 API
-type encode_value() ::
integer() |
float() |
boolean() |
null |
binary() |
atom() |
list(encode_value()) |
#{binary() | atom() | integer() => encode_value()}.
-spec encode(encode_value()) -> iodata().
以及進階的基於回呼的 API,允許單次通過編碼自訂資料結構。此 API 附帶一組有助於實作自訂編碼回呼的函數。
-type encoder() :: fun((dynamic(), encoder()) -> iodata()).
-spec encode(dynamic(), encoder()) -> iodata().
-spec encode_value(dynamic(), encoder()) -> iodata().
-spec encode_atom(atom(), encoder()) -> iodata().
-spec encode_integer(integer()) -> iodata().
-spec encode_float(float()) -> iodata().
-spec encode_list(list(), encoder()) -> iodata().
-spec encode_map(map(), encoder()) -> iodata().
-spec encode_map_checked(map(), encoder()) -> iodata().
-spec encode_key_value_list([{dynamic(), dynamic()}], encoder()) -> iodata().
-spec encode_key_value_list_checked([{dynamic(), dynamic()}], encoder()) -> iodata().
-spec encode_binary(binary()) -> iodata().
-spec encode_binary_escape_all(binary()) -> iodata().
encoder()
回呼會在遍歷期間在每個值上叫用。上面指定的簡單 API 等效於使用 fun json:encode_value/2
函數作為編碼器。
函數的 *_checked/2
變體提供驗證編碼器不會產生重複的鍵。預設的 encode_binary/1
函數將發出規範允許的未逸出 Unicode 值;但是,出於相容性原因,我們提供了可選的 encode_binary_escape_all/1
函數,該函數將始終產生純 ASCII 訊息,並使用 \u
逸出序列編碼所有較高的 Unicode 值。
此 EEP 進一步提出用於格式化(和美觀列印)JSON 訊息的其他 API。此 API 包括將文字 JSON 訊息轉換為格式化的 JSON 訊息。這是最靈活的解決方案,可以正交地支援自訂編碼函數的格式化結果(如上所述),而不會在編碼器中間增加複雜的格式化選項的負擔。格式化通常不會在高效能服務的關鍵熱路徑中完成,因此,兩次通過格式化的開銷被認為是可以接受的。
-type format_option() :: #{
indent => iodata(),
line_separator => iodata(),
after_colon => iodata()
}.
-spec format(iodata()) -> iodata().
-spec format(iodata(), format_option()) -> iodata().
PR-8111 實作了此 EEP 中建議的 encode/1
、encode/2
、decode/1
和 decode/3
函數。格式化 API 和對不完整訊息解碼的支援將作為後續任務。
給定以下資料
{"a": [[], {}, true, false, null, {"foo": "baz"}], "b": [1, 2.0, "three"]}
將使用以下引數呼叫解碼 API
object_start(Acc0) => Acc1
string(<<"a">>) => Str1
array_start(Acc1) => Acc2
empty_array() => Arr1
array_push(Acc2, Arr1) => Acc3
empty_object() => Obj1
array_push(Obj1, Acc3) => Acc4
array_push(true, Acc4) => Acc5
array_push(false, Acc5) => Acc6
null() => Null
array_push(Null, Acc6) => Acc7
object_start(Acc7) => Acc8
string(<<"foo">>) => Str2
string(<<"baz">>) => Str3
object_push(Str2, Str3, Acc8) => Acc9
object_finish(Acc9) => Obj2
array_push(Obj2, Acc7) => Acc10
array_finish(Acc10, Acc1) => {Arr1, Acc11}
object_push(Arr1, Acc11) => Acc12
string(<<"b">>) => Str4
array_start(Acc12) => Acc13
integer(<<"1">>) => Int1
array_push(Int1, Acc13) => Acc14
float(<<"2.0">>) => Float1
array_push(Float1, Acc14) => Acc15
string(<<"three">>) => Str5
array_push(Str5, Acc15) => Acc16
array_finish(Acc16, Acc12) => {Arr2, Acc17}
object_push(Str4, Arr2, Acc17) => Acc18
object_finish(Acc18, Acc0) => {Obj3, Acc19}
% final decode/3 return
{Obj3, Acc19, <<"">>}
支援使用啟發式方法來區分類似物件的鍵值對列表與普通值列表的自訂編碼器範例可能如下所示
custom_encode(Value) -> json:encode(Value, fun encoder/2).
encoder([{_, _} | _] = Value, Encode) -> json:encode_key_value_list(Value, Encode);
encoder(Other, Encode) -> json:encode_value(Other, Encode).
另一個支援將 Elixir nil
用作 Null 並使用協定進行進一步自訂的編碼器可能如下所示
encoder(nil, _Encode) -> <<"null">>;
encoder(null, _Encode) -> <<"\"null\"">>;
encoder(#{__struct__ => _} = Struct, Encode) -> 'Elixir.JSONProtocol':encode(Struct, Encode);
encoder(Other, Encode) -> json:encode_value(Other, Encode).
本文檔置於公有領域或 CC0-1.0-Universal 許可證下,以較寬容者為準。