此 EEP 提议在 Erlang 标准库中引入一个模块 json
,以支持将 JSON 文档从 Erlang 数据结构编码和解码。主要原因是弥补 Erlang 标准库在如此广泛流行的通用数据格式方面的空白。
JSON 常用于许多不同的应用场景
已经存在许多用于 Erlang 和其他 BEAM 语言的 JSON 库,但是将这种支持添加到标准库中将提供独特的好处。最值得注意的是能够在利用第三方库复杂或繁琐的情况下使用它——例如独立的 escript 或像构建系统这样的基本工具,或者在 OTP 本身内部。
之前曾尝试将 JSON 支持引入 OTP,最著名的是 EEP 18,但由于各种原因最终没有被采用。但是,我认为现在是重新审视这个问题的时候了,并对这种支持可能采取的接口进行新的思考。
JSON 是一种在 RFC 8259 和 ECMA 404 中并行定义的格式,但是如何将这种表示形式转换为 Erlang 并不完全清楚,因为数据结构不是直接的 1:1 映射。为了解决这个问题,此 EEP 提出了一个接口,该接口提供了一个方便且“规范”的简单 API,以及一个具有通用底层实现的可扩展且高度可自定义的 API。
此 EEP 提出了一个 JSON 库,它
提议的 JSON 库将提供
我们建议,在“规范” API 中,以以下方式将 JSON 数据结构映射到 Erlang 并映射回来
从 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
解码为另一个原子,特别是 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
: 原子 null
我们建议对完整的 decode/3
API 进行未来的增强,它可以返回一个 {incomplete, continuation()}
值,该值可用于解码跨多个二进制 blob 分割的值(例如从 TCP 套接字接收的值)。
-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
用作空值并使用协议进行进一步自定义的编码器如下所示
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 许可证下,以更宽松者为准。