json-decoder
Version:
Lightweight, lightning-fast, type safe JSON decoder for TypeScript
240 lines (169 loc) โข 10.9 kB
Markdown
# TypeScript JSON Decoder: `json-decoder`
**`json-decoder`** is a type safe compositional JSON decoder for `TypeScript`. It is heavily inspired by [Elm](https://package.elm-lang.org/packages/elm/json/latest/) and [ReasonML](https://github.com/glennsl/bs-json) JSON decoders. The code is loosely based on [aische/JsonDecoder](https://github.com/aische/JsonDecoder) but is a full rewrite, and does not rely on unsafe `any` type.
[](https://travis-ci.org/venil7/json-decoder) [](https://github.com/ellerbrock/typescript-badges/)
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:
```TypeScript
const result: Result<string> = stringDecoder.decode("some string"); //Ok("some string");
const result: Result<string> = stringDecoder.decode(123.45); //Err("string expected");
```
- `numberDecoder` - decodes a number:
```TypeScript
const result: Result<number> = numberDecoder.decode(123.45); //Ok(123.45);
const result: Result<number> = numberDecoder.decode("some string"); //Err("number expected");
```
- `boolDecoder` - decodes a boolean:
```TypeScript
const result: Result<boolean> = boolDecoder.decode(true); //Ok(true);
const result: Result<boolean> = boolDecoder.decode(null); //Err("bool expected");
```
- `nullDecoder` - decodes a `null` value:
```TypeScript
const result: Result<null> = nullDecoder.decode(null); //Ok(null);
const result: Result<null> = boolDecoder.decode(false); //Err("null expected");
```
- `undefinedDecoder` - decodes an `undefined` value:
```TypeScript
const result: Result<undefined> = undefinedDecoder.decode(undefined); //Ok(undefined);
const result: Result<undefined> = boolDecoder.decode(null); //Err("undefined expected");
```
- `arrayDecoder<T>(decoder: Decoder<T>)` - decodes an array, requires one parameter of array item decoder:
```TypeScript
const numberArrayDecoder = arrayDecoder(numberDecoder);
const result: Result<number[]> = numberArrayDecoder.decode([1,2,3]); //Ok([1,2,3]);
const result: Result<number[]> = numberArrayDecoder.decode("some string"); //Err("array expected");
const result: Result<number[]> = numberArrayDecoder.decode([true, false, null]); //Err("array: number expected");
```
- `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.
```TypeScript
type Pet = {name: string, age: number};
const petDecoder = objectDecoder<Pet>({
name: stringDecoder,
age: numberDecoder,
});
const result: Result<Pet> = petDecoder.decode({name: "Varia", age: 0.5}); //Ok({name: "Varia", age: 0.5});
const result: Result<Pet> = petDecoder.decode({name: "Varia", type: "cat"}); //Err("name: string expected");
const petDecoder = objectDecoder<Pet>({
name: stringDecoder,
type: stringDecoder, //<-- error: field type is not defined in Pet
});
```
- `exactDecoder<T>(value: T)` - 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<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.
```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<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`.
```TypeScript
const catDecoder = exactDecoder("cat");
const result: Result<"cat"> = allOfDecoders(stringSecoder, catDecoder); //Ok("cat")
```
## Type inference
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<typeof numberDecoder>; //number
const someDecoder = objectDecoder({
field1: stringDecoder,
field2: numberDecoder,
field3: arrayDecoder(numberDecoder)
});
type Some = DecoderType<typeof someDecoder>; // {field1: string, field2: number, field3: number[] }
const some: Some = await someDecoder.decodeAsync({...});
const stringOrNumberDecoder = oneOfDecoders<string |number>(stringDecoder, numberDecoder);
type StringOrNumber = DecoderType<typeof stringOrNumberDecoder>; //string | number
```
## 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:
```TypeScript
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](https://wiki.haskell.org/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<T2>(bindFunc: (t: T) => Decoder<T2>): Decoder<T2>` - allows for [monadic](https://wiki.haskell.org/Monad) (think >>=) chaining of decoders. Takes a function, that given a result of previous decoding return a new decoder of type `Decoder<T2>`.
- `then<T2>(nextDecoder: Decoder<T2>): Decoder<T2>` - allows to chain several decoders one after the other, is an equivalent of calling `allOfDecoders(thisDecoder, nextDecoder)`
## Custom decoder
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)
```
## Result and pattern matching
Decoding can either succeed or fail, to denote that `json-decoder` has [ADT](https://en.wikipedia.org/wiki/Algebraic_data_type) type `Result<T>`, which can take two forms:
- `Ok<T>` - carries a succesfull decoding result of type `T`, use `.value` to access value
- `Err<T>` - 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<string> = Ok("cat").map(s => s.toUpperCase()); //Ok("CAT")
const e: Result<string> = Err("some error").map(s => s.toUpperCase()); //Err("some error")
```
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<Person>
switch (result.type) {
case OK: result.value; // Person
case Err: result.message; // message string
}
```
## Friendly errors
Errors emit exact decoder expectations where decoding whent wrong, even for deeply nested objects and arrays
## Mapping and type conversion
- **simple type converson** - is possible with `.map` and chaining decoder, see `floatDecoder` as an example
- **more comlex conditional** decoding is possible using `.bind` 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
const decoder = oneOfDecoders<string | number>(
stringDecoder,
numberDecoder
).bind<string | number>((t: string | number) =>
typeof t == "string"
? stringDecoder.map((s) => `${s}!!`)
: numberDecoder.map((n) => n * 2)
);
```
## Validation
`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<number> = numberDecoder.validate(n => Math.floor(n) === n, "not an integer");
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<number> = stringDecoder.validate(/^\S+@\S+$/.test, "not an email");
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
const emailDecoder: Decoder<number> = stringDecoder.validate(/^\S+@\S+$/.test, (invalidEmail) => `${invalidEmail} not an email`);
const email = emailDecoder.decode("joe@example.com"); //Ok("joe@example.com")
const notEmail = emailDecoder.decode("joe"); //Err("joe is not an email")
```
## Contributions are welcome
Please raise an issue or create a PR