548 lines
16 KiB
Elixir
548 lines
16 KiB
Elixir
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
|
|
defprotocol JSON.Encoder do
|
|
@moduledoc """
|
|
A protocol for custom JSON encoding of data structures.
|
|
|
|
If you have a struct, you can derive the implementation of this protocol
|
|
by specifying which fields should be encoded to JSON:
|
|
|
|
@derive {JSON.Encoder, only: [...]}
|
|
defstruct ...
|
|
|
|
Additionally, you can exclude specific fields using the `:except` option or
|
|
encode all fields by omitting both options entirely, but these should be used
|
|
with caution:
|
|
|
|
@derive {JSON.Encoder, except: [...]}
|
|
defstruct ...
|
|
|
|
@derive JSON.Encoder
|
|
defstruct ...
|
|
|
|
> #### Leaking Private Information {: .error}
|
|
>
|
|
> Prefer using `:only` to avoid accidentally leaking private information when
|
|
> new fields are added. Other approaches should be used with auction.
|
|
|
|
You can also use `Protocol.derive/3` if you don't own the struct that you want
|
|
to encode to JSON:
|
|
|
|
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
|
|
Protocol.derive(JSON.Encoder, NameOfTheStruct, except: [...])
|
|
Protocol.derive(JSON.Encoder, NameOfTheStruct)
|
|
|
|
"""
|
|
|
|
@undefined_impl_description """
|
|
the protocol must be explicitly implemented.
|
|
|
|
If you have a struct, you can derive the implementation specifying \
|
|
which fields should be encoded to JSON:
|
|
|
|
@derive {JSON.Encoder, only: [....]}
|
|
defstruct ...
|
|
|
|
It is also possible to encode all fields, although this should be \
|
|
used carefully to avoid accidentally leaking private information \
|
|
when new fields are added:
|
|
|
|
@derive JSON.Encoder
|
|
defstruct ...
|
|
|
|
Finally, if you don't own the struct you want to encode to JSON, \
|
|
you may use Protocol.derive/3 placed outside of any module:
|
|
|
|
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
|
|
Protocol.derive(JSON.Encoder, NameOfTheStruct)\
|
|
"""
|
|
|
|
@impl true
|
|
defmacro __deriving__(module, opts) do
|
|
fields = module |> Macro.struct_info!(__CALLER__) |> Enum.map(& &1.field)
|
|
fields = fields_to_encode(fields, opts)
|
|
vars = Macro.generate_arguments(length(fields), __MODULE__)
|
|
kv = Enum.zip(fields, vars)
|
|
|
|
{io, _prefix} =
|
|
Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix ->
|
|
key = IO.iodata_to_binary([prefix, :elixir_json.encode_binary(Atom.to_string(field)), ?:])
|
|
{[key, quote(do: encoder.(unquote(value), encoder))], ?,}
|
|
end)
|
|
|
|
io = if io == [], do: "{}", else: io ++ [?}]
|
|
|
|
quote do
|
|
defimpl JSON.Encoder, for: unquote(module) do
|
|
def encode(%{unquote_splicing(kv)}, encoder) do
|
|
unquote(io)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
defp fields_to_encode(fields, opts) do
|
|
cond do
|
|
only = Keyword.get(opts, :only) ->
|
|
case only -- fields do
|
|
[] ->
|
|
only
|
|
|
|
error_keys ->
|
|
raise ArgumentError,
|
|
"unknown struct fields #{inspect(error_keys)} specified in :only. Expected one of: " <>
|
|
"#{inspect(fields -- [:__struct__])}"
|
|
end
|
|
|
|
except = Keyword.get(opts, :except) ->
|
|
case except -- fields do
|
|
[] ->
|
|
fields -- [:__struct__ | except]
|
|
|
|
error_keys ->
|
|
raise ArgumentError,
|
|
"unknown struct fields #{inspect(error_keys)} specified in :except. Expected one of: " <>
|
|
"#{inspect(fields -- [:__struct__])}"
|
|
end
|
|
|
|
true ->
|
|
fields -- [:__struct__]
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
A function invoked to encode the given term to `t:iodata/0`.
|
|
"""
|
|
def encode(term, encoder)
|
|
end
|
|
|
|
defimpl JSON.Encoder, for: Atom do
|
|
def encode(value, encoder) do
|
|
case value do
|
|
nil -> "null"
|
|
true -> "true"
|
|
false -> "false"
|
|
_ -> encoder.(Atom.to_string(value), encoder)
|
|
end
|
|
end
|
|
end
|
|
|
|
defimpl JSON.Encoder, for: BitString do
|
|
def encode(value, _encoder) do
|
|
:elixir_json.encode_binary(value)
|
|
end
|
|
end
|
|
|
|
defimpl JSON.Encoder, for: List do
|
|
def encode(value, encoder) do
|
|
:elixir_json.encode_list(value, encoder)
|
|
end
|
|
end
|
|
|
|
defimpl JSON.Encoder, for: Integer do
|
|
def encode(value, _encoder) do
|
|
:elixir_json.encode_integer(value)
|
|
end
|
|
end
|
|
|
|
defimpl JSON.Encoder, for: Float do
|
|
def encode(value, _encoder) do
|
|
:elixir_json.encode_float(value)
|
|
end
|
|
end
|
|
|
|
defimpl JSON.Encoder, for: Map do
|
|
def encode(value, encoder) do
|
|
case :maps.next(:maps.iterator(value)) do
|
|
:none ->
|
|
"{}"
|
|
|
|
{key, value, iterator} ->
|
|
[?{, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)]
|
|
end
|
|
end
|
|
|
|
defp next(iterator, encoder) do
|
|
case :maps.next(iterator) do
|
|
:none ->
|
|
"}"
|
|
|
|
{key, value, iterator} ->
|
|
[?,, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)]
|
|
end
|
|
end
|
|
|
|
# Erlang supports only numbers, binaries, and atoms as keys,
|
|
# we support anything that implements the String.Chars protocol.
|
|
defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
|
|
defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
|
|
defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
|
|
end
|
|
|
|
defimpl JSON.Encoder, for: Duration do
|
|
def encode(value, _encoder) do
|
|
[?", Duration.to_iso8601(value), ?"]
|
|
end
|
|
end
|
|
|
|
defimpl JSON.Encoder, for: Date do
|
|
def encode(%{calendar: Calendar.ISO} = date, _encoder) do
|
|
%{year: year, month: month, day: day} = date
|
|
[?", Calendar.ISO.date_to_iodata(year, month, day), ?"]
|
|
end
|
|
|
|
def encode(value, _encoder) do
|
|
[?", Date.to_iso8601(value), ?"]
|
|
end
|
|
end
|
|
|
|
defimpl JSON.Encoder, for: Time do
|
|
def encode(%{calendar: Calendar.ISO} = time, _encoder) do
|
|
%{
|
|
hour: hour,
|
|
minute: minute,
|
|
second: second,
|
|
microsecond: microsecond
|
|
} = time
|
|
|
|
[?", Calendar.ISO.time_to_iodata(hour, minute, second, microsecond), ?"]
|
|
end
|
|
|
|
def encode(value, _encoder) do
|
|
[?", Time.to_iso8601(value), ?"]
|
|
end
|
|
end
|
|
|
|
defimpl JSON.Encoder, for: NaiveDateTime do
|
|
def encode(%{calendar: Calendar.ISO} = naive_datetime, _encoder) do
|
|
%{
|
|
year: year,
|
|
month: month,
|
|
day: day,
|
|
hour: hour,
|
|
minute: minute,
|
|
second: second,
|
|
microsecond: microsecond
|
|
} = naive_datetime
|
|
|
|
[
|
|
?",
|
|
Calendar.ISO.date_to_iodata(year, month, day),
|
|
?T,
|
|
Calendar.ISO.time_to_iodata(hour, minute, second, microsecond),
|
|
?"
|
|
]
|
|
end
|
|
|
|
def encode(value, _encoder) do
|
|
[?", NaiveDateTime.to_iso8601(value), ?"]
|
|
end
|
|
end
|
|
|
|
defimpl JSON.Encoder, for: DateTime do
|
|
def encode(%{calendar: Calendar.ISO} = datetime, _encoder) do
|
|
%{
|
|
year: year,
|
|
month: month,
|
|
day: day,
|
|
hour: hour,
|
|
minute: minute,
|
|
second: second,
|
|
microsecond: microsecond,
|
|
time_zone: time_zone,
|
|
utc_offset: utc_offset,
|
|
std_offset: std_offset
|
|
} = datetime
|
|
|
|
[
|
|
?",
|
|
Calendar.ISO.date_to_iodata(year, month, day),
|
|
?T,
|
|
Calendar.ISO.time_to_iodata(hour, minute, second, microsecond),
|
|
Calendar.ISO.offset_to_iodata(utc_offset, std_offset, time_zone, :extended),
|
|
?"
|
|
]
|
|
end
|
|
|
|
def encode(value, _encoder) do
|
|
[?", DateTime.to_iso8601(value), ?"]
|
|
end
|
|
end
|
|
|
|
defmodule JSON.DecodeError do
|
|
@moduledoc """
|
|
The exception raised by `JSON.decode!/1`.
|
|
"""
|
|
defexception [:message, :offset, :data]
|
|
end
|
|
|
|
defmodule JSON do
|
|
@moduledoc ~S"""
|
|
JSON encoding and decoding.
|
|
|
|
Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and
|
|
[ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/)
|
|
standards.
|
|
|
|
## Encoding
|
|
|
|
Elixir primitive types are encoded to JSON as follows:
|
|
|
|
| **Elixir** | **JSON** |
|
|
|----------------------------|----------|
|
|
| `integer() \| float()` | Number |
|
|
| `true \| false ` | Boolean |
|
|
| `nil` | Null |
|
|
| `binary()` | String |
|
|
| `atom()` | String |
|
|
| `list()` | Array |
|
|
| `%{String.Chars.t() => _}` | Object |
|
|
|
|
You may also implement the `JSON.Encoder` protocol for custom data structures.
|
|
Some built-in data-structures already derive the `JSON.Encoder` protocol:
|
|
|
|
| **Elixir** | **JSON** |
|
|
|------------------------|-----------------|
|
|
| `Date.t()` | ISO 8601 string |
|
|
| `Time.t()` | ISO 8601 string |
|
|
| `DateTime.t()` | ISO 8601 string |
|
|
| `NaiveDateTime.t()` | ISO 8601 string |
|
|
| `Duration.t()` | ISO 8601 string |
|
|
|
|
## Decoding
|
|
|
|
Elixir types are decoded from JSON as follows:
|
|
|
|
| **JSON** | **Elixir** |
|
|
|----------|------------------------|
|
|
| Number | `integer() \| float()` |
|
|
| Boolean | `true \| false` |
|
|
| Null | `nil` |
|
|
| String | `binary()` |
|
|
| Object | `%{binary() => _}` |
|
|
|
|
"""
|
|
|
|
@moduledoc since: "1.18.0"
|
|
|
|
@type encoder :: (term(), encoder() -> iodata())
|
|
|
|
@type decode_error_reason ::
|
|
{:unexpected_end, non_neg_integer()}
|
|
| {:invalid_byte, non_neg_integer(), byte()}
|
|
| {:unexpected_sequence, non_neg_integer(), binary()}
|
|
|
|
@typedoc """
|
|
Decoders for customizing JSON decoding behavior.
|
|
"""
|
|
@type decoders :: [
|
|
array_start: (term() -> term()),
|
|
array_push: (term(), term() -> term()),
|
|
array_finish: (term(), term() -> {term(), term()}),
|
|
object_start: (term() -> term()),
|
|
object_push: (term(), term(), term() -> term()),
|
|
object_finish: (term(), term() -> {term(), term()}),
|
|
float: (String.t() -> term()),
|
|
integer: (String.t() -> term()),
|
|
string: (String.t() -> term()),
|
|
null: term()
|
|
]
|
|
|
|
@doc ~S"""
|
|
Decodes the given JSON.
|
|
|
|
Returns `{:ok, decoded}` or `{:error, reason}`.
|
|
|
|
## Examples
|
|
|
|
iex> JSON.decode("[null,123,\"string\",{\"key\":\"value\"}]")
|
|
{:ok, [nil, 123, "string", %{"key" => "value"}]}
|
|
|
|
## Error reasons
|
|
|
|
The error tuple will have one of the following reasons.
|
|
|
|
* `{:unexpected_end, offset}` if `binary` contains incomplete JSON value
|
|
* `{:invalid_byte, offset, byte}` if `binary` contains unexpected byte or invalid UTF-8 byte
|
|
* `{:unexpected_sequence, offset, bytes}` if `binary` contains invalid UTF-8 escape
|
|
"""
|
|
@spec decode(binary()) :: {:ok, term()} | {:error, decode_error_reason()}
|
|
def decode(binary) when is_binary(binary) do
|
|
with {decoded, :ok, rest} <- decode(binary, :ok, []) do
|
|
if rest == "" do
|
|
{:ok, decoded}
|
|
else
|
|
{:error, {:invalid_byte, byte_size(binary) - byte_size(rest), :binary.at(rest, 0)}}
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc ~S"""
|
|
Decodes the given JSON with the given decoders.
|
|
|
|
Returns `{decoded, acc, rest}` or `{:error, reason}`.
|
|
See `decode/1` for the error reasons.
|
|
|
|
## Decoders
|
|
|
|
All decoders are optional. If not provided, they will fall back to
|
|
implementations used by the `decode/1` function:
|
|
|
|
* for `array_start`: `fn _ -> [] end`
|
|
* for `array_push`: `fn elem, acc -> [elem | acc] end`
|
|
* for `array_finish`: `fn acc, old_acc -> {Enum.reverse(acc), old_acc} end`
|
|
* for `object_start`: `fn _ -> [] end`
|
|
* for `object_push`: `fn key, value, acc -> [{key, value} | acc] end`
|
|
* for `object_finish`: `fn acc, old_acc -> {Map.new(acc), old_acc} end`
|
|
* for `float`: `&String.to_float/1`
|
|
* for `integer`: `&String.to_integer/1`
|
|
* for `string`: `&Function.identity/1`
|
|
* for `null`: the atom `nil`
|
|
|
|
For streaming decoding, see Erlang's [`:json`](`:json`) module.
|
|
"""
|
|
@spec decode(binary(), term(), decoders()) ::
|
|
{term(), term(), binary()} | {:error, decode_error_reason()}
|
|
def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) do
|
|
decoders = Keyword.put_new(decoders, :null, nil)
|
|
|
|
try do
|
|
:elixir_json.decode(binary, acc, Map.new(decoders))
|
|
catch
|
|
:error, :unexpected_end ->
|
|
{:error, {:unexpected_end, byte_size(binary)}}
|
|
|
|
:error, {:invalid_byte, byte} ->
|
|
{:error, {:invalid_byte, offset(__STACKTRACE__), byte}}
|
|
|
|
:error, {:unexpected_sequence, bytes} ->
|
|
{:error, {:unexpected_sequence, offset(__STACKTRACE__), bytes}}
|
|
end
|
|
end
|
|
|
|
defp offset(stacktrace) do
|
|
with [{_, _, _, opts} | _] <- stacktrace,
|
|
%{cause: %{position: position}} <- opts[:error_info] do
|
|
position
|
|
else
|
|
_ -> 0
|
|
end
|
|
end
|
|
|
|
@doc ~S"""
|
|
Decodes the given JSON but raises an exception in case of errors.
|
|
|
|
Returns the decoded content. See `decode/1` for possible errors.
|
|
|
|
## Examples
|
|
|
|
iex> JSON.decode!("[null,123,\"string\",{\"key\":\"value\"}]")
|
|
[nil, 123, "string", %{"key" => "value"}]
|
|
"""
|
|
@spec decode!(binary()) :: term()
|
|
def decode!(binary) when is_binary(binary) do
|
|
case decode(binary) do
|
|
{:ok, decoded} ->
|
|
decoded
|
|
|
|
{:error, {:unexpected_end, offset}} ->
|
|
raise JSON.DecodeError,
|
|
message: "unexpected end of JSON binary at position (byte offset) #{offset}",
|
|
data: binary,
|
|
offset: offset
|
|
|
|
{:error, {:invalid_byte, offset, byte}} ->
|
|
raise JSON.DecodeError,
|
|
message: "invalid byte #{byte} at position (byte offset) #{offset}",
|
|
data: binary,
|
|
offset: offset
|
|
|
|
{:error, {:unexpected_sequence, offset, bytes}} ->
|
|
raise JSON.DecodeError,
|
|
message: "unexpected sequence #{inspect(bytes)} at position (byte offset) #{offset}",
|
|
data: binary,
|
|
offset: offset
|
|
end
|
|
end
|
|
|
|
@doc ~S"""
|
|
Encodes the given term to JSON as a binary.
|
|
|
|
The second argument is a function that is recursively
|
|
invoked to encode a term.
|
|
|
|
> #### IO and performance {: .tip}
|
|
>
|
|
> If you need to encode data to be sent over the network
|
|
> or written to the filesystem, consider using the more
|
|
> efficient `encode_to_iodata!/2`.
|
|
|
|
## Examples
|
|
|
|
iex> JSON.encode!([123, "string", %{key: "value"}])
|
|
"[123,\"string\",{\"key\":\"value\"}]"
|
|
|
|
"""
|
|
@spec encode!(term(), encoder()) :: binary()
|
|
def encode!(term, encoder \\ &protocol_encode/2) do
|
|
IO.iodata_to_binary(encoder.(term, encoder))
|
|
end
|
|
|
|
@doc ~S"""
|
|
Encodes the given term to JSON as an iodata.
|
|
|
|
This is the most efficient format if the JSON is going to be
|
|
used for IO purposes.
|
|
|
|
The second argument is a function that is recursively
|
|
invoked to encode a term.
|
|
|
|
## Examples
|
|
|
|
iex> data = JSON.encode_to_iodata!([123, "string", %{key: "value"}])
|
|
iex> IO.iodata_to_binary(data)
|
|
"[123,\"string\",{\"key\":\"value\"}]"
|
|
|
|
"""
|
|
@spec encode_to_iodata!(term(), encoder()) :: iodata()
|
|
def encode_to_iodata!(term, encoder \\ &protocol_encode/2) do
|
|
encoder.(term, encoder)
|
|
end
|
|
|
|
@doc """
|
|
This is the default encode implementation passed to `encode!/1`.
|
|
|
|
This function is most typically passed as second argument to
|
|
`encode!/2` and `encode_to_iodata!/2`. The default implementation
|
|
is an optimized dispatch to the `JSON.Encoder` protocol.
|
|
"""
|
|
@spec protocol_encode(term(), encoder()) :: iodata()
|
|
def protocol_encode(value, encoder) when is_atom(value) do
|
|
case value do
|
|
nil -> "null"
|
|
true -> "true"
|
|
false -> "false"
|
|
_ -> encoder.(Atom.to_string(value), encoder)
|
|
end
|
|
end
|
|
|
|
def protocol_encode(value, _encoder) when is_binary(value),
|
|
do: :elixir_json.encode_binary(value)
|
|
|
|
def protocol_encode(value, _encoder) when is_integer(value),
|
|
do: :elixir_json.encode_integer(value)
|
|
|
|
def protocol_encode(value, _encoder) when is_float(value),
|
|
do: :elixir_json.encode_float(value)
|
|
|
|
def protocol_encode(value, encoder) when is_list(value),
|
|
do: :elixir_json.encode_list(value, encoder)
|
|
|
|
def protocol_encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
|
|
do: JSON.Encoder.Map.encode(value, encoder)
|
|
|
|
def protocol_encode(value, encoder),
|
|
do: JSON.Encoder.encode(value, encoder)
|
|
end
|