json-decoder is a Java 8 library for type-safe JSON decoding, almost a direct port of Elm's Json.Decode.
This page shows the concepts
behind the Json.Decode
elm package, json-decoder
attempts to mimic that API.
The Decoders
class contains simple decoders and facilities to build complex ones.
Once a decoder is built, JSON can be decoded by calling Decoders.decodeString
or Decoders.decodeValue
. These functions will return an Either<String, T>
which will have either an error message on the left, or a successfully decoded
value on the right.
We'll statically import Decoders.*
for brevity. Integer
, String
, etc. are members of that class.
decodeString("1", Integer); // right(1)
decodeString("1", String); // left("expected String, got JNumber{value=1}")
decodeString("\"string\"", String); // right("string")
list
decodes a JSON array and decodes every element with a given decoder. Returns a javaslang List<T>
.
index
decodes an array and picks the element at a given index.
decodeString("[1, 2, 3]", list(Integer)); // right(List.of(1, 2, 3))
decodeString("[1, 2, \"a\"]", index(2, String)); // right("a")
dict
decodes a Map<String, T>
given a Decoder<T>
.
decodeString("{\"a\": 1, \"b\": 2, \"c\": 3}", dict(Integer)); // right(HashMap.of("a", 1, "b", 2, "c", 3))
Enums can be parsed by attempting to match a string exactly.
decodeString("\"ERA\"", enumByName(ChronoField.class)); // right(ChronoField.ERA)
map
can be used on a decoder to transform a decoded value. Like changing the container:
Decoder<LinkedList<Integer>> linkedList = list(Integer)
.map(ints -> ints.toJavaCollection(LinkedList::new));
decodeString("[1, 2, 3]", linkedList); // right(LinkedList(1, 2, 3))
Or computing some other value, like the sum of an array:
Decoder<Integer> sum = list(Integer).map(ints -> ints.fold(0, (z, x) -> z + x));
decodeString("[1, 2, 3]", sum); // right(6)
field
decodes an object and accesses a field within it, fails if the field is missing.
at
traverses an object tree.
decodeString("{\"a\": \"b\"}", field("a", String)); // right("b")
decodeString("{\"a\": \"b\"}", field("b", String)); // left("field 'b': missing")
decodeString("{\"a\": {\"b\": \"c\"} }", at(List.of("a", "b"), String)); // right("c")
Decoders for complex structures can be built by composing other decoders with map<N>
,
where N
is the number of decoders:
// with the following class:
public class Person {
final String name;
final int age;
// constructor
}
Decoder<Person> personDecoder = Decoder.map2(
field("name", String),
field("age", Integer),
Person::new
);
decodeString("{\"name\":\"jack\",\"age\":18}", personDecoder); // right(Person("jack", 18))
decodeString("{\"name\":\"jack\"}", personDecoder); // left("field 'age': missing")
option
will try to use a given encoder and return the result inside an Option
,
if the decoder fails it just returns Option.none()
. Therefore, it will never fail.
optionalField
only succeeds if the field is missing or the field exists and
the inner decoder succeeds as well.
decodeString("1", option(Integer)); // right(Option.of(1))
decodeString("1", option(String)); // right(Option.none()) -- notice that decoding did not fail
In the following case, both optionalField
and option
return none
:
decodeString("{\"b\": 1}", optionalField("a", String)); // right(Option.none())
decodeString("{\"b\": 1}", option(field("a", String))); // right(Option.none())
However, in this case option
will silently ignore the unexpected number, while optionalField
will fail
decodeString("{\"a\": 1}", optionalField("a", String)); // left("field 'a': expected String, got JNumber{value=1}")
decodeString("{\"a\": 1}", option(field("a", String))); // right(Option.none())
oneOf
attempts multiple decoders, nullValue
returns a given value if null
is found.
decodeString(
"[1, \"hello\", null]",
list(oneOf(
Integer.map(i -> i.toString()), // we use `map` to turn `Integer` into a String decoder
String,
nullValue("<missing>"))));
// right(List.of("1", "hello", "<missing>"))
nullable
allows null values, returns an Option<T>
.
decodeString("[1, 2, null]", list(nullable(Integer))); // right(List.of(some(1), some(2), none()))
andThen
can be used to apply a decoder after another (to the same JSON value), here are some examples:
Deciding on a parser based on a result:
Decoder<String> versionedDecoder = field("ver", Integer)
.andThen(version ->
version == 0 ? field("name", String) :
version == 1 ? field("fullName", String) :
fail("unknown version " + version));
decodeString("{\"ver\":0,\"name\":\"john\"}", versionedDecoder); // right("john")
decodeString("{\"ver\":2,\"name\":\"john\"}", versionedDecoder); // left("unknown version 2");
First we decode the object and test if it has a ver
, then we pick a decoder based on ver
and apply it to the same object.
Extending a decoder to validate decoded values:
Decoder<String> nonEmptyString = String
.andThen(str -> str.isEmpty()
? fail("empty string")
: succeed(str));
decodeString("\"ok\"", nonEmptyString); // right("ok")
decodeString("\"\"", nonEmptyString); // left("empty string")
We first attempt to decode a string, and then return a failing decoder with a message if it's empty, or a successful decoder otherwise.
Here is an example of using andThen
to build a Decoder<T>
when T
is abstract.
recursive
can be used to build a decoder that references itself. This is necessary because Java lambdas can't reference this
.
// given this Tree:
public class Tree<T> {
final T v;
final List<Tree<T>> children;
// constructor
}
Decoder<Tree<Integer>> intTreeDecoder =
recursive(self ->
Decoder.map2(
field("v", Integer),
optionalField("children", list(self)).map(optList -> optList.getOrElse(List.empty())),
Tree::new));
String json = "{ \"v\": 1" +
", \"children\": [ { \"v\": 2 }" +
", { \"v\": 3, \"children\": [ { \"v\": 4 } ] }" +
"]" +
"}";
decodeString(json, intTreeDecoder); // right(tree(1, tree(2), tree(3, tree(4))))
More examples can be found in the tests.
From jcenter:
repositories {
jcenter()
}
compile 'com.fredhonorio:json-decoder:1.2.1'
json-decoder uses immutable-json-ast for the JSON AST, jackson for parsing JSON and javaslang for utility.
This project is licensed under the Apache License v2.0.