作者
Michał Muskała <micmus(at)whatsapp(dot)com>
狀態
最終/27.0 已在 OTP 27 版本中實作
類型
標準追蹤
建立於
12-02-2024
Erlang 版本
OTP-27.0
發布歷史
https://github.com/erlang/otp/pull/8111
取代
EEP-0018

EEP 68:JSON 函式庫 #

摘要 #

此 EEP 提議將一個 json 模組引入 Erlang 標準函式庫,支援從 Erlang 資料結構編碼和解碼 JSON 文件。主要原因是要彌補 Erlang 標準函式庫在這種廣泛流行且普遍使用的資料格式方面的不足。

理由 #

JSON 在許多不同的使用案例中很常見

  • 作為網路服務的輕量級且人類可讀的資料交換格式;
  • 作為靜態檔案中的組態語言;
  • 作為開發人員工具的資料交換格式;
  • 等等。

目前有許多 Erlang 和其他 BEAM 語言的 JSON 函式庫,但是將此支援新增到標準函式庫將會帶來獨特的好處。最顯著的是能够在無法輕易使用第三方函式庫的情況下使用它,例如獨立的 escript 或基本工具(如建置系統),或在 OTP 本身內使用。

先前曾嘗試將 JSON 支援引入 OTP,最著名的是 EEP 18,但由於各種原因最終未被採用。但是,我相信現在是時候以新的角度重新檢視這個主題,並考慮這種支援可能採用的介面。

JSON 是在 RFC 8259ECMA 404 中平行規範的完善格式,但是由於資料結構沒有直接的 1:1 對應關係,因此如何將此表示法轉換為 Erlang 並不完全清楚。為了協助處理此問題,此 EEP 提出了一個介面,該介面提供了一個方便且「規範」的簡單 API,以及一個具有常見底層實作的可擴展且高度可自訂的 API。

此 EEP 提出了一個 JSON 函式庫,該函式庫

  • 應該容易在大型程式碼庫中使用,使用現有流行的開放原始碼 JSON 函式庫之一;
  • 將允許具有自訂功能(例如支援 Elixir 協定)的現有開放原始碼函式庫成為此函式庫周圍的輕量包裝器;
  • 將改善或至少不會降低效能,與領先的開放原始碼 JSON 函式庫相比。

提出的 JSON 函式庫將提供

  • JSON 編碼,允許單次通過編碼自訂資料類型——特別是對於 Elixir,透過薄層(在 OTP 外部實作)與協定整合;
  • JSON 解碼,具有一些串流支援,允許解碼無法完全放入記憶體的訊息;
  • JSON 解碼,支援解碼分散在多個獨立訊息中的值,而無需預先完全串連它們;
  • 重點關注高效能編碼和解碼;
  • 完全符合 RFC 8259ECMA 404 標準,解碼器應該通過整個 JSONTestSuite
  • 適用於常見使用案例的簡單 API,具有標準資料類型映射。

設計選擇 #

資料映射 #

我們建議在「標準」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 函式庫中看到的功能

  • 將字串鍵解碼為 atom;
  • 將物件解碼為成對的列表;
  • 將浮點數解碼為具有十進位精度的自訂結構;
  • null 解碼為另一個 atom,特別是 undefinednil
  • 在將保留在記憶體中的字串上使用 binary:copy/1
  • 從單個二進位 Blob 解碼多個 JSON 訊息;
  • 等等。

此外,這允許使用者僅保留部分資料結構,以實現類似於對無法完全放入記憶體的資料使用串流 SAX 剖析器的結果。

array_finishobject_finish 回呼負責還原累加器以繼續處理父物件。為了簡化累加器未連接的情況,這些回呼接收傳遞給相應 _start 呼叫的累加器值。

所有回呼都是可選的,並且具有對應於「簡單」API 行為的預設值,使用列表作為累加器,特別是

  • 對於 array_startfun(_) -> [] end
  • 對於 array_pushfun(Elem, Acc) -> [Elem | Acc] end
  • 對於 array_finishfun(Acc, OldAcc) -> {lists:reverse(Acc), OldAcc} end
  • 對於 object_startfun(_) -> [] end
  • 對於 object_pushfun(Key, Value, Acc) -> [{Key, Value} | Acc] end
  • 對於 object_finishfun(Acc, OldAcc) -> {maps:from_list(Acc), OldAcc} end
  • 對於 floatfun erlang:binary_to_float/1
  • 對於 integerfun erlang:binary_to_integer/1
  • 對於 stringfun (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()}.

編碼 API #

對於編碼,此 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/1encode/2decode/1decode/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 許可證下,以較寬容者為準。