Lightweight, lightning-fast, type safe JSON decoder for TypeScript
npm install json-decoderjson-decoderjson-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 aische/JsonDecoder but is a full rewrite, and does not rely on unsafe any type.
 
Give us a ๐on Github
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.
```
$> npm install json-decoder
$> yarn add json-decoder
Below is a list of basic decoders supplied with json-decoder:
- stringDecoder - decodes a string:
`TypeScript`
const result: Result
const result: Result
- numberDecoder - decodes a number:
`TypeScript`
const result: Result
const result: Result
- boolDecoder - decodes a boolean:
`TypeScript`
const result: Result
const result: Result
- nullDecoder - decodes a null value:
`TypeScript`
const result: Result
const result: Result
- undefinedDecoder - decodes an undefined value:
`TypeScript`
const result: Result
const result: Result
- arrayDecoder - decodes an array, requires one parameter of array item decoder:
`TypeScript`
const numberArrayDecoder = arrayDecoder(numberDecoder);
const result: Result
const result: Result
const result: Result
- objectDecoder - 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.
`TypeScript
type Pet = {name: string, age: number};
const petDecoder = objectDecoder
name: stringDecoder,
age: numberDecoder,
});
const result: Result
const result: Result
const petDecoder = objectDecoder
name: stringDecoder,
type: stringDecoder, //<-- error: field type is not defined in Pet
});
`
- exactDecoder - decodes a value that is passed as a parameter. Any other value will result in Err:
`TypeScript`
const catDecoder = exactDecoder("cat");
const result: Result<"cat"> = catDecoder.decode("cat"); //Ok("cat");
const result: Result<"cat"> = catDecoder.decode("dog"); //Err("cat expected");
- oneOfDecoders - 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.
`TypeScript
const catDecoder = exactDecoder("cat");
const dogDecoder = exactDecoder("dog");
const petDecoder = oneOfDecoders<"cat"|"dog"> = oneOfDecoders(catDecoder, dogDecoder);
const result: Result<"cat"|"dog"> = petDecoder.decode("cat"); //Ok("cat");
const result: Result<"cat"|"dog"> = petDecoder.decode("dog"); //Ok("dog");
const result: Result<"cat"|"dog"> = petDecoder.decode("giraffe"); //Err("none of decoders matched");
`
- allOfDecoders(...decoders: Decoder - 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.
`TypeScript`
const catDecoder = exactDecoder("cat");
const result: Result<"cat"> = allOfDecoders(stringSecoder, catDecoder); //Ok("cat")
Type works both ways - not only you can specify type for a decoder, it is also possible to infer the type from an existing decoder, particularly useful for composition of decoders:
`TypeScript
type Number = DecoderType
const someDecoder = objectDecoder({
field1: stringDecoder,
field2: numberDecoder,
field3: arrayDecoder(numberDecoder)
});
type Some = DecoderType
const some: Some = await someDecoder.decodeAsync({...});
const stringOrNumberDecoder = oneOfDecoders
type StringOrNumber = DecoderType
`
Each decoder has the following methods:
- decode(json:unknown): Result - attempts to decode a value of unknown type. Returns Ok if succesful, Err otherwise.decodeAsync(json:unknown): Promise
- - Returns a Promise that attempts to decode a value of unknown type. Resolves with T if succesful, rejects Error{message:string} otherwise.async
A typical usage of this would be in an function context:
`TypeScript`
const getPet = async (): Promise
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 - each decoder is a functor. Map allows you to apply a function to an underlying decoder 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.
- bind - allows for monadic (think >>=) chaining of decoders. Takes a function, that given a result of previous decoding return a new decoder of type Decoder.
- then - allows to chain several decoders one after the other, is an equivalent of calling allOfDecoders(thisDecoder, nextDecoder)
Customized decoders are possible by combining existing decoders with user defined mapping. For example to create a floatDecoder that decodes valid string:
`TypeScript
const floatDecoder = stringDecoder.map(parseFloat);
const float = floatDecoder.decode("123.45"); //Ok(123.45)
`
Decoding can either succeed or fail, to denote that json-decoder has ADT type Result, which can take two forms:
- Ok - carries a succesfull decoding result of type T, use .value to access valueErr
- - carries an unsuccesfull decoding 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
`TypeScript`
const r: Result
const e: Result
It is possible to pattern-match (using poor man's pattern matching provided by TypeScript) to determite the type of Result
`TypeScript
// assuming some result:Result
switch (result.type) {
case OK: result.value; // Person
case Err: result.message; // message string
}
`
Errors emit exact decoder expectations where decoding whent wrong, even for deeply nested objects and arrays
- simple type converson - is possible with .map and chaining decoder, see floatDecoder as an example.bind
- more comlex conditional decoding is possible using to chain decoders one after the other, with user defined arbitrary combination logic. The following example executes different decoder depending on the result of previous decoder.
`TypeScript${s}!!
const decoder = oneOfDecoders
stringDecoder,
numberDecoder
).bind
typeof t == "string"
? stringDecoder.map((s) => )`
: numberDecoder.map((n) => n * 2)
);
JSON only exposes an handful of types: string, number, null, boolean, array and object. There's no way to enforce special kind of validation on any 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
`TypeScript`
const integerDecoder: Decoder
const integer = integerDecoder.decode(123); //Ok(123)
const float = integerDecoder.decode(123.45); //Err("not an integer")
#### Example: emailDecoder - only decodes a string that matches email regex, fails otherwise
`TypeScript`
const emailDecoder: Decoder
const email = emailDecoder.decode("joe@example.com"); //Ok("joe@example.com")
const notEmail = emailDecoder.decode("joe"); //Err("not an email")
Also decoder.validate can take function as a second parameter. It should have such type: (value: T) => string.
#### Example: emailDecoder - only decodes a string that matches email regex, fails otherwise
`TypeScript${invalidEmail} not an email
const emailDecoder: Decoder);``
const email = emailDecoder.decode("joe@example.com"); //Ok("joe@example.com")
const notEmail = emailDecoder.decode("joe"); //Err("joe is not an email")
Please raise an issue or create a PR