An open API service indexing awesome lists of open source software.

https://github.com/venil7/json-decoder

Type safe JSON decoder for TypeScript
https://github.com/venil7/json-decoder

composition decoder elm elm-lang functional-programming json typescript

Last synced: 3 months ago
JSON representation

Type safe JSON decoder for TypeScript

Awesome Lists containing this project

README

          

# 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.

[![Build Status](https://travis-ci.org/venil7/json-decoder.svg?branch=master)](https://travis-ci.org/venil7/json-decoder) [![TypeScript](https://badges.frapsoft.com/typescript/code/typescript.svg?v=101)](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 = stringDecoder.decode("some string"); //Ok("some string");
const result: Result = stringDecoder.decode(123.45); //Err("string expected");
```

- `numberDecoder` - decodes a number:

```TypeScript
const result: Result = numberDecoder.decode(123.45); //Ok(123.45);
const result: Result = numberDecoder.decode("some string"); //Err("number expected");
```

- `boolDecoder` - decodes a boolean:

```TypeScript
const result: Result = boolDecoder.decode(true); //Ok(true);
const result: Result = boolDecoder.decode(null); //Err("bool expected");
```

- `nullDecoder` - decodes a `null` value:

```TypeScript
const result: Result = nullDecoder.decode(null); //Ok(null);
const result: Result = boolDecoder.decode(false); //Err("null expected");
```

- `undefinedDecoder` - decodes an `undefined` value:

```TypeScript
const result: Result = undefinedDecoder.decode(undefined); //Ok(undefined);
const result: Result = boolDecoder.decode(null); //Err("undefined expected");
```

- `arrayDecoder(decoder: Decoder)` - decodes an array, requires one parameter of array item decoder:

```TypeScript
const numberArrayDecoder = arrayDecoder(numberDecoder);
const result: Result = numberArrayDecoder.decode([1,2,3]); //Ok([1,2,3]);
const result: Result = numberArrayDecoder.decode("some string"); //Err("array expected");
const result: Result = numberArrayDecoder.decode([true, false, null]); //Err("array: number expected");
```

- `objectDecoder(decoderMap: DecoderMap)` - 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 = petDecoder.decode({name: "Varia", age: 0.5}); //Ok({name: "Varia", age: 0.5});
const result: Result = petDecoder.decode({name: "Varia", type: "cat"}); //Err("name: string expected");

const petDecoder = objectDecoder({
name: stringDecoder,
type: stringDecoder, //<-- error: field type is not defined in Pet
});
```

- `exactDecoder(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(...decoders: Decoder[])` - 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[]): 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 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; //number
const someDecoder = objectDecoder({
field1: stringDecoder,
field2: numberDecoder,
field3: arrayDecoder(numberDecoder)
});
type Some = DecoderType; // {field1: string, field2: number, field3: number[] }
const some: Some = await someDecoder.decodeAsync({...});

const stringOrNumberDecoder = oneOfDecoders(stringDecoder, numberDecoder);
type StringOrNumber = DecoderType; //string | number
```

## API

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.
A typical usage of this would be in an `async` 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](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(bindFunc: (t: T) => Decoder): Decoder` - 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`.

- `then(nextDecoder: Decoder): Decoder` - 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`, which can take two forms:

- `Ok` - carries a succesfull decoding result of type `T`, use `.value` to access value
- `Err` - 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 = Ok("cat").map(s => s.toUpperCase()); //Ok("CAT")
const e: Result = 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

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(
stringDecoder,
numberDecoder
).bind((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 = 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 = 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 = 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