Skip to content

Commit 9d5198f

Browse files
committed
Decoding
1 parent caffdfd commit 9d5198f

File tree

2 files changed

+147
-0
lines changed

2 files changed

+147
-0
lines changed

lib/elixir/lib/json.ex

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,117 @@ defmodule JSON do
187187

188188
@moduledoc since: "1.18.0"
189189

190+
@type decode_error ::
191+
{:unexpected_end, non_neg_integer()}
192+
| {:invalid_byte, non_neg_integer(), byte()}
193+
| {:unexpected_sequence, non_neg_integer(), binary()}
194+
195+
@doc ~S"""
196+
Decodes the given JSON.
197+
198+
Returns `{:ok, decoded}` or `{:error, reason}`.
199+
200+
## Examples
201+
202+
iex> JSON.decode("[null,123,\"string\",{\"key\":\"value\"}]")
203+
{:ok, [nil, 123, "string", %{"key" => "value"}]}
204+
205+
## Error reasons
206+
207+
The error tuple will have one of the following reasons.
208+
209+
* `{:unexpected_end, position}` if `binary` contains incomplete JSON value
210+
* `{:invalid_byte, position, byte}` if `binary` contains unexpected byte or invalid UTF-8 byte
211+
* `{:unexpected_sequence, position, bytes}` if `binary` contains invalid UTF-8 escape
212+
"""
213+
@spec decode(binary()) :: {:ok, term()} | decode_error()
214+
def decode(binary) when is_binary(binary) do
215+
with {decoded, :ok, rest} <- decode(binary, :ok, []) do
216+
if rest == "" do
217+
{:ok, decoded}
218+
else
219+
{:error, {:invalid_byte, byte_size(binary) - byte_size(rest), :binary.at(rest, 0)}}
220+
end
221+
end
222+
end
223+
224+
@doc ~S"""
225+
Decodes the given JSON with the given decoders.
226+
227+
Returns `{decoded, acc, rest}` or `{:error, reason}`.
228+
See `decode/1` for the error reasons.
229+
230+
## Decoders
231+
232+
All decoders are optional. If not provided, they will fall back to
233+
implementations used by the `decode/1` function:
234+
235+
* for `array_start`: `fn _ -> [] end`
236+
* for `array_push`: `fn elem, acc -> [elem | acc] end`
237+
* for `array_finish`: `fn acc, old_acc -> {Enum.reverse(acc), old_acc} end`
238+
* for `object_start`: `fn _ -> [] end`
239+
* for `object_push`: `fn key, value, acc -> [{key, value} | acc] end`
240+
* for `object_finish`: `fn acc, old_acc -> {Map.new(acc), old_acc} end`
241+
* for `float`: `&String.to_float/1`
242+
* for `integer`: `&String.to_integer/1`
243+
* for `string`: `&Function.identity/1`
244+
* for `null`: the atom `nil`
245+
246+
For streaming decoding, see Erlang's `:json` module.
247+
"""
248+
@spec decode(binary(), term(), keyword()) :: {term(), term(), binary()} | decode_error()
249+
def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) do
250+
decoders = Keyword.put_new(decoders, :null, nil)
251+
252+
try do
253+
:elixir_json.decode(binary, acc, Map.new(decoders))
254+
catch
255+
:error, :unexpected_end ->
256+
{:error, {:unexpected_end, byte_size(binary)}}
257+
258+
:error, {:invalid_byte, byte} ->
259+
{:error, {:invalid_byte, position(__STACKTRACE__), byte}}
260+
261+
:error, {:unexpected_sequence, bytes} ->
262+
{:error, {:unexpected_sequence, position(__STACKTRACE__), bytes}}
263+
end
264+
end
265+
266+
defp position(stacktrace) do
267+
with [{_, _, _, opts} | _] <- stacktrace,
268+
%{cause: %{position: position}} <- opts[:error_info] do
269+
position
270+
else
271+
_ -> 0
272+
end
273+
end
274+
275+
@doc ~S"""
276+
Decodes the given JSON but raises an exception in case of errors.
277+
278+
Returns the decoded content. See `decode!/1` for possible errors.
279+
280+
## Examples
281+
282+
iex> JSON.decode!("[null,123,\"string\",{\"key\":\"value\"}]")
283+
[nil, 123, "string", %{"key" => "value"}]
284+
"""
285+
def decode!(binary) when is_binary(binary) do
286+
case decode(binary) do
287+
{:ok, decoded} ->
288+
decoded
289+
290+
{:error, {:unexpected_end, position}} ->
291+
raise ArgumentError, "unexpected end of JSON binary at position #{position}"
292+
293+
{:error, {:invalid_byte, position, byte}} ->
294+
raise ArgumentError, "invalid byte #{byte} at position #{position}"
295+
296+
{:error, {:unexpected_sequence, position, bytes}} ->
297+
raise ArgumentError, "unexpected sequence #{inspect(bytes)} at position #{position}"
298+
end
299+
end
300+
190301
@doc ~S"""
191302
Encodes the given term to JSON as a binary.
192303

lib/elixir/test/elixir/json_test.exs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,40 @@ defmodule JSONTest do
135135
assert JSON.encode_to_iodata!(%WithEmpty{}) == "{}"
136136
end
137137
end
138+
139+
describe "decode" do
140+
test "succeeds" do
141+
assert JSON.decode("[null,123,456.7,\"string\",{\"key\":\"value\"}]") ==
142+
{:ok, [nil, 123, 456.7, "string", %{"key" => "value"}]}
143+
144+
assert JSON.decode!("[null,123,456.7,\"string\",{\"key\":\"value\"}]") ==
145+
[nil, 123, 456.7, "string", %{"key" => "value"}]
146+
end
147+
148+
test "unexpected end" do
149+
assert JSON.decode("{") == {:error, {:unexpected_end, 1}}
150+
151+
assert_raise ArgumentError, "unexpected end of JSON binary at position 1", fn ->
152+
JSON.decode!("{")
153+
end
154+
end
155+
156+
test "invalid byte" do
157+
assert JSON.decode(",") == {:error, {:invalid_byte, 0, ?,}}
158+
assert JSON.decode("123o") == {:error, {:invalid_byte, 3, ?o}}
159+
160+
assert_raise ArgumentError, "invalid byte 111 at position 3", fn ->
161+
JSON.decode!("123o")
162+
end
163+
end
164+
165+
test "unexpected sequence" do
166+
assert JSON.decode("\"\\ud8aa\\udcxx\"") ==
167+
{:error, {:unexpected_sequence, 1, "\\ud8aa\\udcxx"}}
168+
169+
assert_raise ArgumentError,
170+
"unexpected sequence \"\\\\ud8aa\\\\udcxx\" at position 1",
171+
fn -> JSON.decode!("\"\\ud8aa\\udcxx\"") end
172+
end
173+
end
138174
end

0 commit comments

Comments
 (0)