TypeScript JSON Decoder: json-decoder
json-decoder
is a type safe compositional JSON decoder for TypeScript
. It is heavily inspired by Elm and ReasonML JSON decoders. The code is loosely based on ts.data.json but is a full rewrite, and does not rely on unsafe any
type.
Give us a 🌟on Github
Compositional decoding
The decoder comprises of small basic building blocks (listed below), that can be composed into JSON decoders of any complexity, including deeply nested structures, heterogenous arrays, etc. If a type can be expressed as TypeScript
interface
or type
(including algebraic data types) - it can be safely decoded and type checked with json-decoder
.
Install (npm or yarn)
$> npm install json-decoder
$> yarn add json-decoder
Basic decoders
Below is a list of basic decoders supplied with json-decoder
:
-
stringDecoder
- decodes a string:
let result: Result<string> = stringDecoder.decode("some string");
let result: Result<string> = stringDecoder.decode(123.45);
-
numberDecoder
- decodes a number:
let result: Result<number> = numberDecoder.decode(123.45);
let result: Result<number> = numberDecoder.decode("some string");
-
boolDecoder
- decodes a boolean:
let result: Result<boolean> = boolDecoder.decode(true);
let result: Result<boolean> = boolDecoder.decode(null);
-
nullDecoder
- decodes a null
value:
let result: Result<null> = nullDecoder.decode(null);
let result: Result<null> = boolDecoder.decode(false);
-
undefinedDecoder
- decodes an undefined
value:
let result: Result<null> = nullDecoder.decode(undefined);
let result: Result<null> = boolDecoder.decode(null);
-
arrayDecoder<T>(decoder: Decoder<T>)
- decodes an array, requires one parameter of array item decoder:
let result: Result<number[]> = arrayDecoder.decode([1,2,3]);
let result: Result<number[]> = arrayDecoder.decode("some string");
let result: Result<number[]> = arrayDecoder.decode([true, false, null]);
-
objectDecoder<T>(decoderMap: DecoderMap<T>)
- decodes an object, requires a decoder map parameter. Decoder map is a composition of decoders, one for each field of an object, that themselves can be object decoders if neccessary.
type Pet = {name: string, age: number};
let petDecoder = objectDecoder<Person>({
name: stringDecoder,
age: numberDecoder,
});
let result: Result<Pet> = petDecoder.decode({name: "Varia", age: 0.5});
let result: Result<Pet> = petDecoder.decode({name: "Varia", type: "cat"});
let petDecoder = objectDecoder<Person>({
name: stringDecoder,
type: stringDecoder,
});
-
exactDecoder<T>(value: T)
- decodes a value that is passed as a parameter. Any other value will result in Err
:
let catDecoder = exactDecoder("cat");
let result: Result<"cat"> = catDecoder.decode("cat");
let result: Result<"cat"> = catDecoder.decode("dog");
-
oneOfDecoders<T1|T2...Tn>(...decoders: Decoder<T1|T2...Tn>[])
- takes a number decoders as parameter and tries to decode a value with each in sequence, returns as soon as one succeeds, errors otherwise. Useful for algebraic data types.
let catDecoder = exactDecoder("cat");
let dogDecoder = exactDecoder("dog");
let petDecoder = oneOfDecoders<"cat"|"dog"> = oneOfDecoders(catDecoder, dogDecoder);
let result: Result<"cat"|"dog"> = petDecoder.decode("cat");
let result: Result<"cat"|"dog"> = petDecoder.decode("dog");
let result: Result<"cat"|"dog"> = petDecoder.decode("giraffe");
-
allOfDecoders(...decoders: Decoder<T1|T2...Tn>[]): Decoder<Tn>
- takes a number decoders as parameter and tries to decode a value with each in sequence, all decoders have to succeed. If at leat one defocer fails - returns Err
.
let catDecoder = exactDecoder("cat");
let result: Result<"cat"> = allOfDecoders(stringSecoder, catDecoder);
API
Each decoder has the following methods:
-
decode(json:unknown): Result<T>
- attempts to decode a value of unknown
type. Returns Ok<T>
if succesful, Err<T>
otherwise.
-
decodeAsync(json:unknown): Promise<T>
- Returns a Promise<T>
that attempts to decode a value of unknown
type. Resolves with T
if succesful, rejects Error{message:string}
otherwise.
A typical usage of this would be in an async
function context:
const getPet = async (): Promise<Pet> => {
const result = await fetch("http://some.pet.api/cat/1");
const pet:Pet = await petDecoder.decodeAsync(await result.json());
return pet;
};
-
map(func: (t:T) => T2) : Decoder<T2>
- each decoder is a functor. Map
allows you to apply a function to an underlying deocoder value, provided that decoding succeeded. Map accepts a function of type (t:T) -> T2
, where T
is a type of decoder (and underlying value), and T2
is a type of resulting decoder.
-
then(bindFunc: (t:T) => Decoder<T2>): Decoder<T2>
- allows for monading chaining of decoders. Takes a function, that returns a Decoder<T2>
, and returns a Decoder<T2>
Custom decoder
Result and pattern matching
Decoding can either succeed or fail, to denote that json-decoder
has ADT type Result<T>
, which can take two forms:
Ok<T>
- carries a succesfull decoding result of type T
, use .value
to access valueErr<T>
- carries an unsuccesfull decodign result of type T
, use .message
to access error message
Result
also has functorial map
function that allows to apply a function to a value, provided that it exists
let r:Result<string> = Ok("cat").map(s => s.toUpperCase);
let e:Result<string> = Err("some error").map(s => s.toUpperCase);
It is possible to pattern-match (using poor man's pattern matching provided by TypeScript) to determite the type of Result
switch (result.type) {
case OK: result.value;
case Err: result.message;
}
Friendly errors
TBC
Mapping and type conversion
TBC
Validation
JSON
only exposes an handful of types: string
, number
, null
, boolean
, array
and object
. There's no way t enforce special kind of validation on ny of above types using just JSON
. json-decoder
allows to validate values against a predicate.
Example: integerDecoder
- only decodes an integer and fails on a float value
let integerDecoder : Decoder<number> = numberDecoder.validate(n => Math.floor(n) === n, "not an integer");
let integer = integerDecoder.decode(123);
let float = integerDecoder.decode(123.45);
Example: emailDecoder
- only decodes a string that matches email regex, fails otherwise
let emailDecoder : Decoder<number> = stringDecoder.validate(/^\S+@\S+$/.test, "not an email");
let email = emailDecoder.decode("joe@example.com");
let notEmail = emailDecoder.decode("joe");
Contributions are welcome
Please raise an issue or create a PR