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
- Host: GitHub
- URL: https://github.com/venil7/json-decoder
- Owner: venil7
- Created: 2019-07-06T10:07:23.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2023-10-19T10:42:13.000Z (almost 2 years ago)
- Last Synced: 2025-06-23T02:49:45.734Z (4 months ago)
- Topics: composition, decoder, elm, elm-lang, functional-programming, json, typescript
- Language: TypeScript
- Homepage: https://darkruby.co.uk/json-decoder/#/
- Size: 2.05 MB
- Stars: 76
- Watchers: 2
- Forks: 4
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Contributing: docs/CONTRIBUTING.md
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.
[](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 = 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:Resultswitch (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