作者
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 提议在 Erlang 标准库中引入一个模块 json,以支持将 JSON 文档从 Erlang 数据结构编码和解码。主要原因是弥补 Erlang 标准库在如此广泛流行的通用数据格式方面的空白。

原理 #

JSON 常用于许多不同的应用场景

  • 作为 Web 服务中一种轻量级且人类可读的数据交换格式;
  • 作为静态文件中的配置语言;
  • 作为开发工具的数据交换格式;
  • 以及更多。

已经存在许多用于 Erlang 和其他 BEAM 语言的 JSON 库,但是将这种支持添加到标准库中将提供独特的好处。最值得注意的是能够在利用第三方库复杂或繁琐的情况下使用它——例如独立的 escript 或像构建系统这样的基本工具,或者在 OTP 本身内部。

之前曾尝试将 JSON 支持引入 OTP,最著名的是 EEP 18,但由于各种原因最终没有被采用。但是,我认为现在是重新审视这个问题的时候了,并对这种支持可能采取的接口进行新的思考。

JSON 是一种在 RFC 8259ECMA 404 中并行定义的格式,但是如何将这种表示形式转换为 Erlang 并不完全清楚,因为数据结构不是直接的 1:1 映射。为了解决这个问题,此 EEP 提出了一个接口,该接口提供了一个方便且“规范”的简单 API,以及一个具有通用底层实现的可扩展且高度可自定义的 API。

此 EEP 提出了一个 JSON 库,它

  • 应该很容易在大型代码库中使用流行的、现有的开源 JSON 库进行采用;
  • 将允许具有自定义功能(如支持 Elixir 协议)的现有开源库成为此库的轻薄包装器;
  • 将提高或至少不会降低与领先的开源 JSON 库相比的性能。

提议的 JSON 库将提供

  • JSON 编码,允许对自定义数据类型进行单次编码——特别是对于 Elixir,通过一个薄层(在 OTP 外部实现)与协议集成;
  • JSON 解码,具有一些流支持,允许解码不完全适合内存的消息;
  • JSON 解码,支持解码分散在单独消息中的值,而无需预先完全连接它们;
  • 专注于高性能编码和解码;
  • 完全符合 RFC 8259ECMA 404 标准,解码器应通过整个 JSONTestSuite
  • 具有规范数据类型映射的常见用例的简单 API。

设计选择 #

数据映射 #

我们建议,在“规范” 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 解码为另一个原子,特别是 undefinednil
  • 在将保留在内存中的字符串上使用 binary:copy/1
  • 从单个二进制 blob 解码多个 JSON 消息;
  • 以及更多。

此外,这允许用户仅保留部分数据结构,以实现类似于使用流式 SAX 类解析器来处理不完全适合内存的数据的结果。

array_finishobject_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()}.

编码 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 用作空值并使用协议进行进一步自定义的编码器如下所示

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 许可证下,以更宽松者为准。