Module dj

dj allows writing composable decoders, combining decoding, transformation and validation.

Description

dj allows writing composable decoders, combining decoding, transformation and validation.

Data Types

decoder()

decoder(T) = fun((jsx:json_term()) -> result(T, errors()))

A decoder(T) is an opaque datastructure that represents a composable decoder. After composing a decoder for your JSON and final datastructure, you can run it with decode/2 or decode/3.

Consider decoder(T) an opaque datastructure. It is only exposed due to dialyzer limitations.

error()

error() = 
    {unexpected_type, ExpectedType :: type(), jsx:json_term()} |
    {missing_field, field(), jsx:json_term()} |
    {missing_index, non_neg_integer(), jsx:json_term()} |
    {in_field, field(), [error(), ...]} |
    {at_index, non_neg_integer(), [error(), ...]} |
    {custom, any()} |
    {invalid_json, any()}

error() is a (resursive) structure that gives detailed information about what went wrong, and where.

Most primitive decoders may fail with an unexpected_type error when the JSON value is not the expected type. Those errors contain both the expected type and the actual value.

When decoders like field/2 or index/2 are used, missing_field and missing_index may be used when the provided field or index was not found in the JSON data.

Errors that occur in the context of a field or index, for example when using field/2 or list/1, are nested in in_field or at_index which allows tracing the failure path.

Convenience and extended decoders using fail/1 will wrap their errors in custom to indicate a higher level failure.

If the actual JSON cannot be parsed by jsx, an invalid_json error is produced.

errors()

errors() = [error(), ...]

A nonempty list of error/0s

field()

field() = atom() | binary()

Depending on the options passed to decoder/3, a field-name may be either an atom(), a binary() or a mix of both. When using decoder/2, field-names are always atoms. Note that those atoms must already exist, or decoding will fail.

result()

result(V, E) = {ok, V} | {error, E}

Running a decoder results in a result, indicating either success (and providing the decoded data) or failure, with a nonempty list of errors.

type()

type() = 
    binary | integer | pos_integer | neg_integer |
    non_neg_integer | null | boolean | float | map | list |
    nonempty_list

These may appear in unexpected_type errors.

Function Index

binary/0Decodes a JSON string as a binary().
integer/0Decodes a JSON integer as an integer()
pos_integer/0Decodes a strictly positive JSON integer as a pos_integer().
neg_integer/0Decodes a negative JSON integer as a neg_integer().
non_neg_integer/0Decodes a positive JSON integer as a non_neg_integer().
float/0Decodes a JSON number as a float()
null/0Equivalent to null(null).
null/1Decodes null into an arbitrary value.
boolean/0Decodes a JSON true or false to a boolean()
value/0Extracts a raw jsx:json_term()
atom/0Decodes a JSON value to an atom()
atom/1Decodes a JSON value to one of a set of predefined atoms.
existing_atom/0Decodes a JSON value to an existing atom.
existing_atom/1Equivalent to atom(Allowed).
email/0Decodes a JSON string as a binary() if and only if it looks like an email address.
full_date_tuple/1
uuid/1Decode a UUIDv4 from a JSON string.
integer/2Decode a bounded integer from JSON.
nullable/1Decode a nullable value.
nullable/2Decode a nullable value.
field/2Instruct a decoder to match a value in a given field.
optional_field/3Decodes a field or uses a default value when the field is missing.
at/2Instruct a decode to match a value in a nested path.
prop/2Decode a field as a property.
prop_list/1Decode a proplist by matching fields on a JSON object.
to_map/1Decode arbitrary JSON into a map()
list/1Decode a JSON list using a decoder.
nonempty_list/1Decode a nonempty JSON list into a nonempty list [T, ..]
index/2Decode a single index in a JSON list using the specified decoder.
sequence/1Sequence a bunch of decoders, succeeding with the collected values.
set/1Decode a JSON list into an erlang sets:set(T)
map/2Manipulate the values produced by a decoder with a function.
chain/2Chain one or more functions that create decoders onto a decoder.
fail/1Create a decoder that always fails with the provided term.
succeed/1Create a decoder that always succeeds with the provided term.
mapn/2Apply an n-ary function against a list of n decoders.
exactly/2Decoding succeeds if the decoder produces exactly the supplied value.
one_of/1Try a bunch of decoders.
decode/2Equivalent to dj:decode(Json, Decoder, [{labels, attempt_atom}]).
decode/3Run a decoder(T) against arbirary JSON.

Function Details

binary/0

binary() -> decoder(binary())

Decodes a JSON string as a binary().

     {ok, <<"Hi there">>} = dj:decode(<<"\"Hi there\"">>, dj:binary()).

If the specified JSON is not a string (it might, for example, be an integer), this will return an error indicating that an unexpected type was found - including the actual value that was found instead. This value will be a jsx:json_term().

     {error, {dj_errors, [{unexpected_type, binary, 123}]}} =
         dj:decode(<<"123">>, dj:binary()).

integer/0

integer() -> decoder(integer())

Decodes a JSON integer as an integer()

     {ok, 123} = dj:decode(<<"123">>, dj:integer()).
     {error, {dj_errors, [{unexpected_type, integer, true}]}} =
         dj:decode(<<"true">>, dj:integer()).

See also: float/0, integer/2, neg_integer/0, non_neg_integer/0, pos_integer/0.

pos_integer/0

pos_integer() -> decoder(pos_integer())

Decodes a strictly positive JSON integer as a pos_integer()

See also: float/0, integer/0, neg_integer/0, non_neg_integer/0.

neg_integer/0

neg_integer() -> decoder(neg_integer())

Decodes a negative JSON integer as a neg_integer()

See also: float/0, integer/0, non_neg_integer/0, pos_integer/0.

non_neg_integer/0

non_neg_integer() -> decoder(non_neg_integer())

Decodes a positive JSON integer as a non_neg_integer()

See also: float/0, integer/0, neg_integer/0, pos_integer/0.

float/0

float() -> decoder(float())

Decodes a JSON number as a float()

Note that JSON does not have a separate floating point type. As such, integers in JSON will be cast to floats by this function.

     {ok, 123.0} = dj:decode(<<"123">>, dj:float()).

See also: integer/0, neg_integer/0, non_neg_integer/0, pos_integer/0.

null/0

null() -> decoder(null)

Equivalent to null(null).

null/1

null(V) -> decoder(V)

Decodes null into an arbitrary value.

This can be used to convert null to a specific value, like undefined or a default value that makes sense for your application.

     {ok, foo} = dj:decode(<<"null">>, dj:null(foo)).

See also: null/0.

boolean/0

boolean() -> decoder(boolean())

Decodes a JSON true or false to a boolean()

     {ok, true} = dj:decode(<<"true">>, dj:boolean()).
     {ok, false} = dj:decode(<<"false">>, dj:boolean()).
     {error, _} = dj:decode(<<"null">>, dj:boolean()).

value/0

value() -> decoder(jsx:json_term())

Extracts a raw jsx:json_term()

atom/0

atom() -> decoder(atom())

Decodes a JSON value to an atom()

If the JSON value is true, false or null, this returns an erlang atom true, false or null. If the JSON value is a string, binary_to_atom(Json, utf8) is used to turn it into an atom.

NOTE: Be careful with this function, as it may be used to force erlang to create many, many new atoms, to the point of running out of memory.

See also: atom/1, existing_atom/0.

atom/1

atom(Allowed :: [atom(), ...]) -> decoder(atom())

Decodes a JSON value to one of a set of predefined atoms

This is a safer alternative to atom(), as it not only allows whitelisting allowed values, but can also prevent creating new atoms.

existing_atom/0

existing_atom() -> decoder(atom())

Decodes a JSON value to an existing atom

Note that Erlang, in some cases, may optimize atoms away. For example, if an atom is only every used in an atom_to_binary(some_atom) call, the some_atom atom may not "exist".

See also: atom/1.

existing_atom/1

existing_atom(Allowed :: [atom(), ...]) -> decoder(atom())

Equivalent to atom(Allowed).

email/0

email() -> decoder(binary())

Decodes a JSON string as a binary() if and only if it looks like an email address.

     {ok, <<"ilias@truqu.com">>} =
         dj:decode(<<"\"ilias@truqu.com\"">>, dj:email()).

If the specified JSON is not a string, this will fail with an unexpected_type error (expecing a binary). If the specified JSON is a string but does not look like an email address, this will fail with a custom not_an_email error.

     E = {dj_errors, [{custom, {not_an_email, <<"foo@bar">>}}]},
     {error, E} = dj:decode(<<"\"foo@bar\"">>, dj:email()).

full_date_tuple/1

full_date_tuple(X1 :: rfc3339) -> decoder(calendar:date())

uuid/1

uuid(X1 :: v4) -> decoder(binary())

Decode a UUIDv4 from a JSON string

integer/2

integer(Min :: integer(), Max :: integer()) -> decoder(integer())

Decode a bounded integer from JSON

Occasionally, you may want to decode a JSON integer only when it sits between certain bounds. Both the upper and lower bound are inclusive.

     -spec score() -> dj:decoder(1..10)
     score() ->
         dj:integer(1, 10).
 
     {ok, 5} = dj:decode(<<"5">>, score()).
 
     E = {dj_errors, [{custom, {integer_out_of_bounds, 1, 10, 0}}]},
     {error, E} = dj:decode(<<"0", score()).

See also: integer/0.

nullable/1

nullable(Decoder :: decoder(T)) -> decoder(T | null)

Equivalent to nullable(Decoder, null).

Decode a nullable value

Sometimes, we explicitly want to allow null. In such a case, making that clear by wrapping a decoder with nullable/1 can help readability.

     -spec score() -> dj:decoder(1..10)
     score() ->
         dj:integer(1, 10).
 
     {ok, 5} = dj:decode(<<"5">>, nullable(score())).
     {ok, null} = dj:decode(<<"null">>, nullable(score())).
 
     E = {dj_error, [ {unexpected_type, integer, true}
                    , {unexpected_type, null, true}
                    ]},
     {error, E} = dj:decode(<<"true", nullable(score())).

nullable/2

nullable(Decoder :: decoder(T), V) -> decoder(T | V)

Equivalent to nullable(Decoder, null).

Decode a nullable value

Sometimes, we explicitly want to allow null but use a default value. In such a case, making that clear by wrapping a decoder with nullable/2 can help readability.

     -spec score() -> dj:decoder(1..10)
     score() ->
         dj:integer(1, 10).
 
     {ok, 4} = dj:decode(<<"4">>, nullable(score(), 5)).
     {ok, 5} = dj:decode(<<"null">>, nullable(score(), 5)).
 
     E = {dj_error, [ {unexpected_type, integer, true}
                    , {unexpected_type, null, true}
                    ]},
     {error, E} = dj:decode(<<"true", nullable(score(), 5)).

field/2

field(Field :: field(), Decoder :: decoder(T)) -> decoder(T)

Instruct a decoder to match a value in a given field

     Dec = dj:field(foo, dj:binary()),
     {ok, <<"bar">>} = dj:decode(<<"{\"foo\": \"bar\"}">>, Dec),
 
     Error = {unexpected_type, binary, null},
     InField = {in_field, foo, Error},
     {error, {dj_errors, [InField]}} = dj:decode(<<"{\"foo\": null}">>, Dec),
 
     Missing = {missing_field, foo, #{}},
     {error, {dj_erros, [Missing]}} = dj:decode(<<"{}">>, Dec).

See also: at/2, prop/2, prop_list/1, to_map/1.

optional_field/3

optional_field(Field :: field(), Decoder :: decoder(T), V) ->
                  decoder(T | V)

Decodes a field or uses a default value when the field is missing

Note that if the field is present but malformed according to the decoder, decoding will fail. If we're not working in the context of a map/object, decoding also fails.

     Dec = dj:optional_field(foo, dj:binary(), <<"default">>),
 
     {ok, <<"bar">>} = dj:decode(<<"{\"foo\": \"bar\"}">>, Dec),
     {ok, <<"default">>} = dj:decode(<<"{}">>, Dec),
 
     Error = {unexpected_type, binary, null},
     InField = {in_field, foo, [Error]},
     {error, {dj_error, [InField]}} = dj:decode(<<"{\"foo\": null}">>, Dec),
 
     NotMap = {unexpected_type, map, <<"foo">>},
     {error, {dj_error, [NotMap]}} = dj:decode(<<"\"foo\"">>, Dec).

at/2

at(Path :: [field()], Decoder :: decoder(T)) -> decoder(T)

Instruct a decode to match a value in a nested path

     {ok, null} = dj:decode(<<"null">>, dj:at([], dj:null())).
 
     Dec = dj:at([foo, bar], dj:decode(dj:integer())),
     Json = <<"{\"foo\": {\"bar\": 123}}">>,
     {ok, 123} = dj:decode(Json, Dec).

See also: field/2, prop/2, prop_list/1, to_map/1.

prop/2

prop(Field :: field(), Decoder :: decoder(T)) ->
        decoder({field(), T})

Decode a field as a property

Similar to field/2 but adds the fieldname to the decoded entry. Convenient for decoding into a proplist.

See also: field/2, prop_list/1.

prop_list/1

prop_list(Spec :: [{field(), decoder(T)}]) ->
             decoder([{field(), T}])

Decode a proplist by matching fields on a JSON object

to_map/1

to_map(MapSpec) -> decoder(MapResult)

Decode arbitrary JSON into a map()

     Dec = dj:to_map(#{ x => dj:index(0, dj:integer())
                      , y => dj:index(1, dj:integer())
                      , z => dj:index(2, dj:integer())
                      }),
     Json = <<"[1, 6, 2]">>,
     {ok, #{x := 1, y := 6, z := 2}} = dj:decode(Json, Dec).

See also: field/2.

list/1

list(Decoder :: decoder(T)) -> decoder([T])

Decode a JSON list using a decoder

nonempty_list/1

nonempty_list(T) -> decoder([T, ...])

Decode a nonempty JSON list into a nonempty list [T, ..]

index/2

index(Index :: non_neg_integer(), Decoder :: decoder(T)) ->
         decoder(T)

Decode a single index in a JSON list using the specified decoder

sequence/1

sequence(Decoders :: [decoder(T)]) -> decoder([T])

Sequence a bunch of decoders, succeeding with the collected values

set/1

set(Decoder :: decoder(T)) -> decoder(sets:set(T))

Decode a JSON list into an erlang sets:set(T)

map/2

map(F :: fun((A) -> B), Decoder :: decoder(A)) -> decoder(B)

Manipulate the values produced by a decoder with a function

     Dec = dj:map(fun string:uppercase/1, dj:binary()),
     Json = <<"\"hello world\"">>,
     {ok, <<"HELLO WORLD">>} = dj:decode(Json, Dec).

See also: chain/2, mapn/2.

chain/2

chain(DecoderA :: decoder(A), ToDecoderB) -> decoder(B)

Chain one or more functions that create decoders onto a decoder

This has many uses. One possible use is to handle data that may represent different things:

     -type shape() :: {square, pos_integer()}
                    | {oblong, pos_integer(), pos_integer()}.
 
     -spec square() -> dj:decoder(shape()).
     square() ->
         dj:map(fun (S) -> {square, S} end, dj:field(side, dj:pos_integer())).
 
     -spec oblong() -> dj:decoder(shape()).
     oblong() ->
         dj:map( fun(L, W) -> {oblong, L, W} end
               , [ dj:field(length, dj:pos_integer())
                 , dj:field(width, dj:pos_integer())
                 ]
               ).
 
     -spec shape(square | oblong) -> dj:decoder(shape()).
     shape(square) -> square();
     shape(oblong) -> oblong().
 
     -spec shape() -> dj:decoder(shape()).
     shape() ->
         dj:chain(dj:field(type, dj:atom([square, oblong])), fun shape/1).
 
     {ok, {square, 12}} =
         dj:decode(<<"{\"type\": \"square\", \"side\": 12}">>, shape()).

Occasionally, you may want to chain an operation that doesn't result in a different decoder, but rather results in either failure or success. In that case, use succeed/1 or fail/1.

When more than one function needs to be chained (for example, a function to pattern match, and another function to validate the structure), the second argument may be a list of functions.

See also: fail/1, map/2, succeed/1.

fail/1

fail(E :: term()) -> decoder(V :: term())

Create a decoder that always fails with the provided term

     Dec = dj:fail(no_more_bananas),
     {error, {dj_errors, [{custom, no_more_bananas}]}}
       = dj:decode(<<"true">>, Dec).
Mostly useful when combined with chain.

See also: chain/2, succeed/1.

succeed/1

succeed(T) -> decoder(T)

Create a decoder that always succeeds with the provided term

     Dec = dj:one_of([ dj:field(online, dj:boolean())
                     , dj:succeed(false)
                     ]),
     Json = << "[ {\"online\": true}"
             , ", {\"online\": false}"
             , ", {} ]"
            >>,
     {ok, [true, false, false]} = dj:decode(Json, dj:list(Dec)).
This function is also useful for hardcoding values in to_map/1, handling failure and success in chain/2 and - as demonstrated - defaulting values using one_of/1.

See also: chain/2, fail/1, one_of/1, to_map/1.

mapn/2

mapn(Fun, Decoders :: [decoder(T)]) -> decoder(V)

Apply an n-ary function against a list of n decoders

     Dec = dj:mapn( fun (X, Y, Z) -> {X, Y, Z} end
                  , [ dj:field(major, dj:pos_integer())
                    , dj:field(minor, dj:non_neg_integer())
                    , dj:field(patch, dj:non_neg_integer())
                    ]
                  ),
     Json = << "{ \"major\": 123"
             , ", \"minor\": 66"
             , ", \"patch\": 0"
             , "}">>,
     {ok, {123, 66, 0}} = dj:decode(Json, Dec).

When the arity doesn't match, a custom error is returned with the expected arity (based on the number of decoders passed) and the actual arity of the passed function.

     Dec = dj:mapn( fun (X, Y) -> {X, Y, 0} end
                  , [ dj:field(major, dj:pos_integer())
                    , dj:field(minor, dj:non_neg_integer())
                    , dj:field(patch, dj:non_neg_integer())
                    ]
                  ),
     Json = << "{ \"major\": 123"
             , ", \"minor\": 66"
             , ", \"patch\": 0"
             , "}">>,
     {error, {dj_errors, [{custom, {arity_mismatch, 3, 2}}]}}
       = dj:decode(Json, Dec).

See also: chain/2, map/2.

exactly/2

exactly(V, Decoder :: decoder(V)) -> decoder(V)

Decoding succeeds if the decoder produces exactly the supplied value.

This can, for example, be used when a certain field is used to switch between different decoders.

one_of/1

one_of(Decoders :: [decoder(V), ...]) -> decoder(V)

Try a bunch of decoders. The first one to succeed will be used.

If all decoders fails, the errors are accumulated.

     Dec = dj:one_of([dj:binary(), dj:integer()]),
     {ok, <<"foo">>} = dj:decode(<<"\"foo\"">>, Dec),
     {ok, 123} = dj:decode(<<"123">>, Dec),
     {error, _} = dj:decode(<<"null">>, Dec).

decode/2

decode(Json, Decoder :: decoder(T)) ->
          result(T, {dj_error, errors()})

Equivalent to dj:decode(Json, Decoder, [{labels, attempt_atom}]).

decode/3

decode(Json, Decoder :: decoder(T), Opts) ->
          result(T, {dj_error, errors()})

Run a decoder(T) against arbirary JSON.

The resulting result(T, error()) is either a tuple {ok, T} or a tuple {error, error()} where error() represents whatever went wrong during the decoding/validation/transformation process.

Opts` are passed on to `jsx:decode/2. The option return_maps is always added by dj and does not need to be specified manually.

Use of the functions that create decoder(T)s and functions that help with composition are discussed individually.


Generated by EDoc