Home

Awesome

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 aische/JsonDecoder but is a full rewrite, and does not rely on unsafe any type.

Build Status TypeScript

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:

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:

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:

Custom decoder

Customized decoders are possible by combining existing decoders with user defined mapping. For example to create a floatDecoder that decodes valid string:

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 type Result<T>, which can take two forms:

Result also has functorial map function that allows to apply a function to a value, provided that it exists

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

// 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

  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

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

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

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