228 lines
8.2 KiB
Markdown
228 lines
8.2 KiB
Markdown
# Poison
|
|
|
|
[](https://travis-ci.org/devinus/poison)
|
|
[](https://coveralls.io/github/devinus/poison?branch=master)
|
|
[](https://hex.pm/packages/poison)
|
|
[](https://hex.pm/packages/poison)
|
|
|
|
Poison is a new JSON library for Elixir focusing on wicked-fast **speed**
|
|
without sacrificing **simplicity**, **completeness**, or **correctness**.
|
|
|
|
Poison takes several approaches to be the fastest JSON library for Elixir.
|
|
|
|
Poison uses extensive [sub binary matching][1], a **hand-rolled parser** using
|
|
several techniques that are [known to benefit HiPE][2] for native compilation,
|
|
[IO list][3] encoding and **single-pass** decoding.
|
|
|
|
Poison benchmarks sometimes puts Poison's performance close to `jiffy` and
|
|
usually faster than other Erlang/Elixir libraries.
|
|
|
|
Poison fully conforms to [RFC 7159][4], [ECMA 404][5], and the
|
|
[JSONTestSuite][6].
|
|
|
|
## Installation
|
|
|
|
First, add Poison to your `mix.exs` dependencies:
|
|
|
|
```elixir
|
|
def deps do
|
|
[{:poison, "~> 3.1"}]
|
|
end
|
|
```
|
|
|
|
Then, update your dependencies:
|
|
|
|
```sh-session
|
|
$ mix deps.get
|
|
```
|
|
|
|
## Usage
|
|
|
|
```elixir
|
|
Poison.encode!(%{"age" => 27, "name" => "Devin Torres"})
|
|
#=> "{\"name\":\"Devin Torres\",\"age\":27}"
|
|
|
|
Poison.decode!(~s({"name": "Devin Torres", "age": 27}))
|
|
#=> %{"age" => 27, "name" => "Devin Torres"}
|
|
|
|
defmodule Person do
|
|
@derive [Poison.Encoder]
|
|
defstruct [:name, :age]
|
|
end
|
|
|
|
Poison.encode!(%Person{name: "Devin Torres", age: 27})
|
|
#=> "{\"name\":\"Devin Torres\",\"age\":27}"
|
|
|
|
Poison.decode!(~s({"name": "Devin Torres", "age": 27}), as: %Person{})
|
|
#=> %Person{name: "Devin Torres", age: 27}
|
|
|
|
Poison.decode!(~s({"people": [{"name": "Devin Torres", "age": 27}]}),
|
|
as: %{"people" => [%Person{}]})
|
|
#=> %{"people" => [%Person{age: 27, name: "Devin Torres"}]}
|
|
```
|
|
|
|
Every component of Poison (encoder, decoder, and parser) are all usable on
|
|
their own without buying into other functionality. For example, if you were
|
|
interested purely in the speed of parsing JSON without a decoding step, you
|
|
could simply call `Poison.Parser.parse`.
|
|
|
|
## Parser
|
|
|
|
```iex
|
|
iex> Poison.Parser.parse!(~s({"name": "Devin Torres", "age": 27}), %{})
|
|
%{"name" => "Devin Torres", "age" => 27}
|
|
iex> Poison.Parser.parse!(~s({"name": "Devin Torres", "age": 27}), %{keys: :atoms!})
|
|
%{name: "Devin Torres", age: 27}
|
|
```
|
|
|
|
Note that `keys: :atoms!` reuses existing atoms, i.e. if `:name` was not
|
|
allocated before the call, you will encounter an `argument error` message.
|
|
|
|
You can use the `keys: :atoms` variant to make sure all atoms are created as
|
|
needed. However, unless you absolutely know what you're doing, do **not** do
|
|
it. Atoms are not garbage-collected, see
|
|
[Erlang Efficiency Guide](http://www.erlang.org/doc/efficiency_guide/commoncaveats.html)
|
|
for more info:
|
|
|
|
> Atoms are not garbage-collected. Once an atom is created, it will never be
|
|
> removed. The emulator will terminate if the limit for the number of atoms
|
|
> (1048576 by default) is reached.
|
|
|
|
## Encoder
|
|
|
|
```iex
|
|
iex> Poison.Encoder.encode([1, 2, 3], %{}) |> IO.iodata_to_binary
|
|
"[1,2,3]"
|
|
```
|
|
|
|
Anything implementing the Encoder protocol is expected to return an
|
|
[IO list][7] to be embedded within any other Encoder's implementation and
|
|
passable to any IO subsystem without conversion.
|
|
|
|
```elixir
|
|
defimpl Poison.Encoder, for: Person do
|
|
def encode(%{name: name, age: age}, options) do
|
|
Poison.Encoder.BitString.encode("#{name} (#{age})", options)
|
|
end
|
|
end
|
|
```
|
|
|
|
For maximum performance, make sure you `@derive [Poison.Encoder]` for any
|
|
struct you plan on encoding.
|
|
|
|
### Encoding only some attributes
|
|
|
|
When deriving structs for encoding, it is possible to select or exclude
|
|
specific attributes. This is achieved by deriving `Poison.Encoder` with the
|
|
`:only` or `:except` options set:
|
|
|
|
```elixir
|
|
defmodule PersonOnlyName do
|
|
@derive {Poison.Encoder, only: [:name]}
|
|
defstruct [:name, :age]
|
|
end
|
|
|
|
defmodule PersonWithoutName do
|
|
@derive {Poison.Encoder, except: [:name]}
|
|
defstruct [:name, :age]
|
|
end
|
|
```
|
|
|
|
In case both `:only` and `:except` keys are defined, the `:except` option is
|
|
ignored.
|
|
|
|
### Key Validation
|
|
|
|
According to [RFC 7159][4] keys in a JSON object should be unique. This is
|
|
enforced and resolved in different ways in other libraries. In the Ruby JSON
|
|
library for example, the output generated from encoding a hash with a duplicate
|
|
key (say one is a string, the other an atom) will include both keys. When
|
|
parsing JSON of this type, Chromium will override all previous values with the
|
|
final one.
|
|
|
|
Poison will generate JSON with duplicate keys if you attempt to encode a map
|
|
with atom and string keys whose encoded names would clash. If you'd like to
|
|
ensure that your generated JSON doesn't have this issue, you can pass the
|
|
`strict_keys: true` option when encoding. This will force the encoding to fail.
|
|
|
|
*Note:* Validating keys can cause a small performance hit.
|
|
|
|
```iex
|
|
iex> Poison.encode!(%{:foo => "foo1", "foo" => "foo2"}, strict_keys: true)
|
|
** (Poison.EncodeError) duplicate key found: :foo
|
|
```
|
|
|
|
## Benchmarking
|
|
|
|
```sh-session
|
|
$ MIX_ENV=bench mix run bench/run.exs
|
|
```
|
|
|
|
### Current Benchmarks
|
|
|
|
As of 2017-05-15 on a 2.8 GHz Intel Core i7:
|
|
|
|
```
|
|
## EncoderBench
|
|
benchmark name iterations average time
|
|
maps (jiffy) 500000 7.88 µs/op
|
|
structs (Poison) 200000 9.46 µs/op
|
|
structs (Jazz) 100000 15.43 µs/op
|
|
structs (JSX) 100000 18.45 µs/op
|
|
maps (Poison) 100000 19.45 µs/op
|
|
maps (Jazz) 100000 21.61 µs/op
|
|
maps (JSX) 50000 31.76 µs/op
|
|
maps (JSON) 50000 34.08 µs/op
|
|
structs (JSON) 50000 47.56 µs/op
|
|
strings (jiffy) 10000 107.68 µs/op
|
|
lists (Poison) 10000 120.79 µs/op
|
|
string escaping (jiffy) 10000 139.92 µs/op
|
|
lists (jiffy) 10000 229.18 µs/op
|
|
lists (Jazz) 10000 236.86 µs/op
|
|
strings (JSON) 10000 237.97 µs/op
|
|
strings (JSX) 10000 283.87 µs/op
|
|
lists (JSX) 5000 336.96 µs/op
|
|
jiffy 5000 429.92 µs/op
|
|
strings (Jazz) 5000 430.78 µs/op
|
|
jiffy (pretty) 5000 431.55 µs/op
|
|
lists (JSON) 5000 559.31 µs/op
|
|
strings (Poison) 5000 574.26 µs/op
|
|
string escaping (Jazz) 1000 1313.51 µs/op
|
|
string escaping (JSX) 1000 1474.66 µs/op
|
|
Poison 1000 1546.53 µs/op
|
|
string escaping (Poison) 1000 1728.66 µs/op
|
|
Poison (pretty) 1000 1784.37 µs/op
|
|
Jazz 1000 2060.77 µs/op
|
|
JSON 1000 2250.89 µs/op
|
|
JSX 1000 2252.77 µs/op
|
|
Jazz (pretty) 1000 2317.55 µs/op
|
|
JSX (pretty) 500 5577.33 µs/op
|
|
## ParserBench
|
|
benchmark name iterations average time
|
|
UTF-8 unescaping (jiffy) 50000 60.05 µs/op
|
|
UTF-8 unescaping (Poison) 10000 112.53 µs/op
|
|
UTF-8 unescaping (JSX) 10000 282.83 µs/op
|
|
UTF-8 unescaping (JSON) 5000 469.26 µs/op
|
|
jiffy 5000 479.07 µs/op
|
|
Poison 5000 730.85 µs/op
|
|
JSX 1000 1947.77 µs/op
|
|
JSON 500 5175.11 µs/op
|
|
Issue 90 (jiffy) 100 18864.70 µs/op
|
|
Issue 90 (Poison) 50 50091.16 µs/op
|
|
Issue 90 (JSX) 10 155975.20 µs/op
|
|
Issue 90 (JSON) 1 1964860.00 µs/op
|
|
```
|
|
|
|
## License
|
|
|
|
Poison is released under [CC0-1.0][8].
|
|
|
|
[1]: http://www.erlang.org/euc/07/papers/1700Gustafsson.pdf
|
|
[2]: http://www.erlang.org/workshop/2003/paper/p36-sagonas.pdf
|
|
[3]: http://jlouisramblings.blogspot.com/2013/07/problematic-traits-in-erlang.html
|
|
[4]: https://tools.ietf.org/html/rfc7159
|
|
[5]: http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf
|
|
[6]: https://github.com/nst/JSONTestSuite
|
|
[7]: http://prog21.dadgum.com/70.html
|
|
[8]: https://creativecommons.org/publicdomain/zero/1.0/
|