Skip to content

An incredibly flexible and performant JSON parser, generator and formatter in pure Erlang.

License

Notifications You must be signed in to change notification settings

williamthome/euneus

Euneus

Github Actions Coverage Erlang Versions Latest Release Hex Version Hex Docs Total Download License Last Updated

An incredibly flexible and performant JSON parser, generator and formatter in pure Erlang.

Euneus is built on the top of the new OTP json module.

Both encoder and decoder fully conform to RFC 8259 and ECMA 404 standards and are tested using JSONTestSuite.

Detailed examples and further explanation can be found at hexdocs.

Requirements

OTP >= 24.

Why should you use Euneus over the OTP json module?

The new OTP json module is incredible and blazing fast!

Unfortunately, it is only available for OTP >= 27. Euneus is available from OTP >= 24.

Also, Euneus simplifies a lot of overheads with the new OTP json module without losing any option provided by the json module and keeping its performance.

A simple example comparing the OTP json module with Euneus decoding object keys:

> json:decode(<<"{\"foo\":\"bar\"}">>, [], #{object_push => fun(K, V, Acc) -> [{binary_to_atom(K), V} | Acc] end}).
{#{foo => <<"bar">>},[],<<>>}
> euneus:decode(<<"{\"foo\":\"bar\"}">>, #{object_keys => atom}).
#{foo => <<"bar">>}

Encode Features

Some reasons to use Euneus for JSON encoding:

  • Possibility to skip values

  • Encoding proplists (proplists are not encoded by the OTP json module)

  • Sort object keys

  • Simple custom encoding via codecs:

    -type codec() ::
        timestamp
        | datetime
        | ipv4
        | ipv6
        | {records, #{Name :: atom() := {Fields :: [atom()], Size :: pos_integer()}}}
        | codec_fun()
        | custom_codec().

Encode Codecs

Encode timestamp
> euneus:encode({0, 0, 0}, #{codecs => [timestamp]}).
<<"\"1970-01-01T00:00:00.000Z\"">>
Encode datetime
> euneus:encode({{1970, 01, 01}, {00, 00, 00}}, #{codecs => [datetime]}).
<<"\"1970-01-01T00:00:00Z\"">>
Encode ipv4
> euneus:encode({0, 0, 0, 0}, #{codecs => [ipv4]}).
<<"\"0.0.0.0\"">>
Encode ipv6
> euneus:encode({16#fe80, 0, 0, 0, 16#204, 16#acff, 16#fe17, 16#bf38}, #{codecs => [ipv6]}).
<<"\"fe80::204:acff:fe17:bf38\"">>
Encode record
% -record(foo, {foo, bar}).
> euneus:encode(#foo{foo = 1, bar = 2}, #{
    codecs => [
      {records, #{
        foo => {record_info(fields, foo), record_info(size, foo)}
      }}
    ]
  }).
<<"{\"foo\":1,\"bar\":2}">>

Decode Features

Some reasons to use Euneus for JSON decoding:

  • Faster decoding than the OTP json module via some options:

    #{
      array_finish => reversed,
      object_finish => reversed_proplist % or proplist
    }
  • The overhead of transforming binary keys to, e.g., atoms

  • Simple custom decoding via codecs:

    -type codec() ::
      copy
      | timestamp
      | datetime
      | ipv4
      | ipv6
      | pid
      | port
      | reference
      | codec_callback().

Decode Object keys

Keys to binary (default)
> euneus:decode(<<"{\"foo\":\"bar\"}">>).
#{<<"foo">> => <<"bar">>}
Keys copy

Just do a binary copy of the key.

> euneus:decode(<<"{\"foo\":\"bar\"}">>, #{object_keys => copy}).
#{<<"foo">> => <<"bar">>}
Keys to atom
> euneus:decode(<<"{\"foo\":\"bar\"}">>, #{object_keys => atom}).
#{foo => <<"bar">>}
Keys to existing atom
> euneus:decode(<<"{\"foo\":\"bar\"}">>, #{object_keys => existing_atom}).
#{foo => <<"bar">>}

Decode Codecs

Decode copy

Just do a binary copy of the value.

> euneus:decode(<<"\"foo\"">>, #{codecs => [copy]}).
<<"foo">>
Decode timestamp
> euneus:decode(<<"\"1970-01-01T00:00:00.000Z\"">>, #{codecs => [timestamp]}).
{0,0,0}
Decode datetime
> euneus:decode(<<"\"1970-01-01T00:00:00Z\"">>, #{codecs => [datetime]}).
{{1970,1,1},{0,0,0}}
Decode ipv4
> euneus:decode(<<"\"0.0.0.0\"">>, #{codecs => [ipv4]}).
{0,0,0,0}
Decode ipv6
> euneus:decode(<<"\"::\"">>, #{codecs => [ipv6]}).
{0,0,0,0,0,0,0,0}
> euneus:decode(<<"\"::1\"">>, #{codecs => [ipv6]}).
{0,0,0,0,0,0,0,1}
> euneus:decode(<<"\"::192.168.42.2\"">>, #{codecs => [ipv6]}).
{0,0,0,0,0,0,49320,10754}
> euneus:decode(<<"\"fe80::204:acff:fe17:bf38\"">>, #{codecs => [ipv6]}).
{65152,0,0,0,516,44287,65047,48952}
Decode pid
> euneus:decode(<<"\"<0.92.0>\"">>, #{codecs => [pid]}).
<0.92.0>
Decode port
> euneus:decode(<<"\"#Port<0.1>\"">>, #{codecs => [port]}).
#Port<0.1>
Decode reference
> euneus:decode(<<"\"#Ref<0.314572725.1088159747.110918>\"">>, #{codecs => [reference]}).
#Ref<0.314572725.1088159747.110918>

Installation

Erlang

% rebar.config
{deps, [
    {json_polyfill, "~> 0.2"}, % Required only for OTP < 27
    {euneus, "~> 2.4"}
]}.

Elixir

# mix.exs
defp deps do
  [
    {:json_polyfill, "~> 0.2"}, # Required only for OTP < 27
    {:euneus, "~> 2.4"}
  ]
end

Basic usage

1> euneus:encode(#{age => 68, name => <<"Joe Armstrong">>, nationality => <<"British">>}).
<<"{\"name\":\"Joe Armstrong\",\"age\":68,\"nationality\":\"British\"}">>
2> euneus:decode(v(1)).
#{<<"age">> => 68,<<"name">> => <<"Joe Armstrong">>,<<"nationality">> => <<"British">>}

Encode

The functions euneus:encode/1 euneus:encode/2 encodes an Erlang term into a binary JSON. The second argument of euneus:encode/2 are options.

Please see the m:euneus_encoder documentation for more examples and detailed explanation.

The data mapping and error reasons can be found in the OTP json encode function documentation.

Decode

The functions euneus:decode/1 and euneus:decode/2 decodes a binary JSON into an Erlang term. The second argument of euneus:decode/2 are options.

Please see the m:euneus_decoder documentation for more examples and detailed explanation.

The data mapping and error reasons can be found in the OTP json decode function documentation.

Stream

Three functions provide JSON decode streaming:

  • euneus:decode_stream_start/1 - Equivalent to euneus:decode_stream_start(JSON, #{});
  • euneus:decode_stream_start/2 - Begin parsing a stream of bytes of a JSON value;
  • euneus:decode_stream_continue/2 - Continue parsing a stream of bytes of a JSON value.

Please see the m:euneus_decoder documentation for more examples and detailed explanation.

Format

Two functions provide JSON formatting:

  • euneus:minify/1 - Removes any extra spaces and new line characters;
  • euneus:format/2 - Formats the JSON by passing custom options.

Please see the m:euneus_formatter documentation for more examples and detailed explanation.

Benchmark

The benchmarks are implemented very simply, but they are a good start foroptimizing Euneus since no optimizations have been made. You will find the benchmark commands in euneus_benchmarker, and data and tests under the test folder.

Important

For the first benchmark run, bootstrapping erlperf is required:

$ rebar3 as benchmark shell

1> euneus_benchmarker:bootstrap().
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling erlperf
===> Building escript for erlperf...
ok

Results

Setup:

  • OS : Linux
  • CPU: 12th Gen Intel(R) Core(TM) i9-12900HX
  • VM : Erlang/OTP 27 [erts-15.0.1] [source] [64-bit] [smp:24:24] [ds:24:24:10] [async-threads:1] [jit:ns]
$ rebar3 as benchmark shell

1> euneus_benchmarker:encode_benchmark().
Code      ||   Samples       Avg   StdDev    Median      P99  Iteration    Rel
jiffy      1         3        26   36.69%        25       36   38474 us   100%
euneus     1         3        20   38.20%        18       29   49197 us    78%
thoas      1         3        10   36.06%         9       14     100 ms    38%

2> euneus_benchmarker:decode_benchmark().
Code       ||   Samples       Avg   StdDev    Median      P99  Iteration    Rel
euneus      1         3        24    2.44%        24       24   42268 us   100%
jiffy       1         3        19    3.09%        19       19   53589 us    79%
thoas       1         3        14    0.00%        14       14   71452 us    59%

Sponsors

If you like this tool, please consider sponsoring me. I'm thankful for your never-ending support ❤️

I also accept coffees ☕

"Buy Me A Coffee"

License

Copyright (c) 2024 William Fank Thomé

Euneus is 100% open-source and community-driven. All components are available under the Apache 2 License on GitHub.

See LICENSE.md for more information.