Skip to content

Commit 579c2e8

Browse files
committed
Add JSON encoding
1 parent e3bad83 commit 579c2e8

File tree

5 files changed

+1576
-0
lines changed

5 files changed

+1576
-0
lines changed

lib/elixir/lib/json.ex

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
defprotocol JSON.Encoder do
2+
@moduledoc """
3+
A protocol for custom JSON encoding of data structures.
4+
"""
5+
6+
@fallback_to_any true
7+
8+
@doc """
9+
A function invoked to encode the given term.
10+
"""
11+
def encode(term, encoder)
12+
end
13+
14+
defimpl JSON.Encoder, for: Atom do
15+
def encode(value, encoder) do
16+
case value do
17+
nil -> "null"
18+
true -> "true"
19+
false -> "false"
20+
_ -> encoder.(Atom.to_string(value), encoder)
21+
end
22+
end
23+
end
24+
25+
defimpl JSON.Encoder, for: BitString do
26+
def encode(value, _encoder) do
27+
:elixir_json.encode_binary(value)
28+
end
29+
end
30+
31+
defimpl JSON.Encoder, for: List do
32+
def encode(value, encoder) do
33+
:elixir_json.encode_list(value, encoder)
34+
end
35+
end
36+
37+
defimpl JSON.Encoder, for: Integer do
38+
def encode(value, _encoder) do
39+
:elixir_json.encode_integer(value)
40+
end
41+
end
42+
43+
defimpl JSON.Encoder, for: Float do
44+
def encode(value, _encoder) do
45+
:elixir_json.encode_float(value)
46+
end
47+
end
48+
49+
defimpl JSON.Encoder, for: Map do
50+
def encode(value, encoder) do
51+
:elixir_json.encode_map(value, encoder)
52+
end
53+
end
54+
55+
defimpl JSON.Encoder, for: Any do
56+
defmacro __deriving__(module, _struct, opts) do
57+
fields = module |> Macro.struct_info!(__CALLER__) |> Enum.map(& &1.field)
58+
fields = fields_to_encode(fields, opts)
59+
vars = Macro.generate_arguments(length(fields), __MODULE__)
60+
kv = Enum.zip(fields, vars)
61+
62+
{io, _prefix} =
63+
Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix ->
64+
key = IO.iodata_to_binary([prefix, :elixir_json.encode_binary(Atom.to_string(field)), ?:])
65+
{[key, quote(do: encoder.(unquote(value), encoder))], ?,}
66+
end)
67+
68+
io = if io == [], do: "{}", else: io ++ [?}]
69+
70+
quote do
71+
defimpl JSON.Encoder, for: unquote(module) do
72+
def encode(%{unquote_splicing(kv)}, encoder) do
73+
unquote(io)
74+
end
75+
end
76+
end
77+
end
78+
79+
defp fields_to_encode(fields, opts) do
80+
cond do
81+
only = Keyword.get(opts, :only) ->
82+
case only -- fields do
83+
[] ->
84+
only
85+
86+
error_keys ->
87+
raise ArgumentError,
88+
"unknown struct fields #{inspect(error_keys)} specified in :only. Expected one of: " <>
89+
"#{inspect(fields -- [:__struct__])}"
90+
end
91+
92+
except = Keyword.get(opts, :except) ->
93+
case except -- fields do
94+
[] ->
95+
fields -- [:__struct__ | except]
96+
97+
error_keys ->
98+
raise ArgumentError,
99+
"unknown struct fields #{inspect(error_keys)} specified in :except. Expected one of: " <>
100+
"#{inspect(fields -- [:__struct__])}"
101+
end
102+
103+
true ->
104+
fields -- [:__struct__]
105+
end
106+
end
107+
108+
def encode(%_{} = struct, _encoder) do
109+
raise Protocol.UndefinedError,
110+
protocol: @protocol,
111+
value: struct,
112+
description: """
113+
JSON.Encoder protocol must be explicitly implemented for structs
114+
115+
If you own the struct, you can derive the implementation specifying \
116+
which fields should be encoded to JSON:
117+
118+
@derive {JSON.Encoder, only: [....]}
119+
defstruct ...
120+
121+
It is also possible to encode all fields, although this should be \
122+
used carefully to avoid accidentally leaking private information \
123+
when new fields are added:
124+
125+
@derive JSON.Encoder
126+
defstruct ...
127+
128+
Finally, if you don't own the struct you want to encode to JSON, \
129+
you may use Protocol.derive/3 placed outside of any module:
130+
131+
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
132+
Protocol.derive(JSON.Encoder, NameOfTheStruct)
133+
"""
134+
end
135+
136+
def encode(value, _encoder) do
137+
raise Protocol.UndefinedError,
138+
protocol: @protocol,
139+
value: value
140+
end
141+
end
142+
143+
defmodule JSON do
144+
@moduledoc """
145+
JSON encoding and decoding.
146+
147+
Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and
148+
[ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/)
149+
standards.
150+
151+
## Encoding
152+
153+
Elixir built-in data structures are encoded to JSON as follows:
154+
155+
| **Elixir** | **JSON** |
156+
|------------------------|----------|
157+
| `integer() \| float()` | Number |
158+
| `true \| false ` | Boolean |
159+
| `nil` | Null |
160+
| `binary()` | String |
161+
| `atom()` | String |
162+
| `list()` | Array |
163+
| `%{binary() => _}` | Object |
164+
| `%{atom() => _}` | Object |
165+
| `%{integer() => _}` | Object |
166+
167+
You may also implement the `JSON.Encoder` protocol for custom data structures.
168+
169+
## Decoding
170+
171+
Elixir built-in data structures are decoded from JSON as follows:
172+
173+
| **JSON** | **Elixir** |
174+
|----------|------------------------|
175+
| Number | `integer() \| float()` |
176+
| Boolean | `true \| false` |
177+
| Null | `nil` |
178+
| String | `binary()` |
179+
| Object | `%{binary() => _}` |
180+
181+
"""
182+
183+
@moduledoc since: "1.18.0"
184+
185+
@doc ~S"""
186+
Encodes the given term to JSON as a binary.
187+
188+
The second argument is a function that is recursively
189+
invoked to encode a term.
190+
191+
## Examples
192+
193+
iex> JSON.encode([123, "string", %{key: "value"}])
194+
"[123,\"string\",{\"key\":\"value\"}]"
195+
196+
"""
197+
def encode(term, encoder \\ &encode_value/2) do
198+
IO.iodata_to_binary(encoder.(term, encoder))
199+
end
200+
201+
@doc ~S"""
202+
Encodes the given term to JSON as an iodata.
203+
204+
This is the most efficient format if the JSON is going to be
205+
used for IO purposes.
206+
207+
The second argument is a function that is recursively
208+
invoked to encode a term.
209+
210+
## Examples
211+
212+
iex> data = JSON.encode_to_iodata([123, "string", %{key: "value"}])
213+
iex> IO.iodata_to_binary(data)
214+
"[123,\"string\",{\"key\":\"value\"}]"
215+
216+
"""
217+
def encode_to_iodata(term, encoder \\ &encode_value/2) do
218+
encoder.(term, encoder)
219+
end
220+
221+
@doc """
222+
A shortcut for `encode/2` used for compatibility purposes.
223+
224+
If you are targetting the `JSON` module directly, do not use
225+
this function, use `JSON.encode/2` instead. This function will
226+
be deprecated in Elixir v1.22
227+
"""
228+
@doc deprecated: "Use JSON.encode/2 instead"
229+
# TODO: Deprecate on Elixir v1.22
230+
def encode!(term, encoder \\ &encode_value/2) do
231+
encode(term, encoder)
232+
end
233+
234+
@doc """
235+
A shortcut for `encode_to_iodata/2` used for compatibility purposes.
236+
237+
If you are targetting the `JSON` module directly, do not use
238+
this function, use `JSON.encode/2` instead. This function will
239+
be deprecated in Elixir v1.22
240+
"""
241+
@doc deprecated: "Use JSON.encode_to_iodata/2 instead"
242+
# TODO: Deprecate on Elixir v1.22
243+
def encode_to_iodata!(term, encoder \\ &encode_value/2) do
244+
encode_to_iodata(term, encoder)
245+
end
246+
247+
@doc """
248+
This is the default function used to recursively encode each value.
249+
"""
250+
def encode_value(value, encoder) when is_atom(value) do
251+
case value do
252+
nil -> "null"
253+
true -> "true"
254+
false -> "false"
255+
_ -> encoder.(Atom.to_string(value), encoder)
256+
end
257+
end
258+
259+
def encode_value(value, _encoder) when is_binary(value),
260+
do: :elixir_json.encode_binary(value)
261+
262+
def encode_value(value, _encoder) when is_integer(value),
263+
do: :elixir_json.encode_integer(value)
264+
265+
def encode_value(value, _encoder) when is_float(value),
266+
do: :elixir_json.encode_float(value)
267+
268+
def encode_value(value, encoder) when is_list(value),
269+
do: :elixir_json.encode_list(value, encoder)
270+
271+
def encode_value(%{} = value, encoder) when not is_map_key(value, :__struct__),
272+
do: :elixir_json.encode_map(value, encoder)
273+
274+
def encode_value(value, encoder),
275+
do: JSON.Encoder.encode(value, encoder)
276+
end

lib/elixir/scripts/elixir_docs.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ canonical = System.fetch_env!("CANONICAL")
9999
Float,
100100
Function,
101101
Integer,
102+
JSON,
102103
Module,
103104
NaiveDateTime,
104105
Record,
@@ -159,6 +160,7 @@ canonical = System.fetch_env!("CANONICAL")
159160
Protocols: [
160161
Collectable,
161162
Enumerable,
163+
JSON.Encoder,
162164
Inspect,
163165
Inspect.Algebra,
164166
Inspect.Opts,

0 commit comments

Comments
 (0)