Skip to content

emeraldwalk/tsutil-json-decode

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

44 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@tsutil/json-decode Build Status

TypeScript JSON decoders inspired by Elm. Provides type-safe JSON decoding and validation. Useful for enforcing data contracts coming from backend data into a front-end code base written in TypeScript.

The decoders in this library serve 2 primary purposes:

  1. Validate data before it gets assigned to type-safe data models.

    const numberDecoder = Decode.number();
    const data: number = numberDecoder('2'); // this will succeed
    const data2: number = numberDecoder('not a number'); // this will throw a runtime error
  2. Transform data between 2 differing data models.

    import * as Decode from '@tsutil/json-decode';
    
    // Back-end data model
    interface RawUser {
      FIRST_NAME: string,
      LAST_NAME: string,
      AGE: string
    }
    
    // Front-end data model
    interface User {
      first: string,
      last: string,
      age: number
    }
    
    // Sample backend data
    const rawUser: RawUser = {
      FIRST_NAME: 'Jane',
      LAST_NAME: 'Doe',
      AGE: '33'
    };
    
    // Create a decoder that can transform backend data into front-end data
    const userDecoder = Decode.object({
      first: ['FIRST_NAME', Decode.string()],
      last: ['LAST_NAME', Decode.string()],
      age: ['AGE', Decode.number()]
    });
    
    // user type will be inferred as User
    const user = userDecoder(rawUser);

Installation

npm install --save @tsutil/json-decode

Example Usage

Decoders

By default decoders are strict and pass any invalid raw data to the configured error handler.

NOTE: The default error handler will throw an Error if no default value is provided, but custom handlers can be configured as well.

Importing Decoders

import * as Decode from '@tsutil/json-decode';

Valid Values

const booleanDecoder = Decode.boolean();
booleanDecoder('true'); // returns true

const dateDecoder = Decode.date();
dateDecoder('2018-06-15'); // returns Date object

const numberDecoder = Decode.number();
numberDecoder('4'); // returns 4

Invalid values

const booleanDecoder = Decode.boolean();
booleanDecoder('invalid'); // throws an Error

const dateDecoder = Decode.date();
dateDecoder('invalid'); // throws an Error

const numberDecoder = Decode.number();
numberDecoder('invalid'); // throws an Error

Decoders with Default Values

Decoders can optionally be configured with a default value. If so, the default will be returned when there is a parsing error instead of calling the errorHandler.

Decoders using undefined as default value

const booleanDecoder = Decode.boolean(undefined);
booleanDecoder('invalid'); // returns undefined

const dateDecoder = Decode.date(undefined);
dateDecoder('invalid'); // returns undefined

const numberDecoder = Decode.number(undefined);
numberDecoder('invalid'); // returns undefined

Configuration

The default decoder configuration will throw errors when a decoder is passed invalid raw data. This behavior can be overridden.

e.g. To log error to console instead of throwing an error:

import * as Decode from '@tsutil/json-decode';

const decode = Decode.configure({
  errorCallback: (error: Error) => {
    console.log(error);
  }
});

const numberDecoder = decode.number();
const result = numberDecoder('not a number'); // logs error to console

Core Decoders

  • array - converts a raw array into a strong typed array. Takes an item decoder as an argument to decode each item in the array.

    const decoder = Decode.array(Decode.number());
    decoder(['1', '2', '3']); // yields [1, 2, 3]
    decoder({}); // throws an error
  • boolean - parses "booleanish" data into boolean values

    const decoder = Decode.boolean();
    
    // these will all yield true
    decoder('true');
    decoder(true);
    decoder(1);
    decoder('1');
    
    // these will all yield false
    decoder('false');
    decoder(false);
    decoder(0);
    decoder('0');
    
    // throws an error
    decoder('not a boolean');
  • date - converts an ISO date string into a Date object.

    const decoder = Decode.date();
    
    // These will all yield Date objects
    decoder('2018-12-15');
    decoder('2018-12-15T00:00:00');
    decoder('2018-12-15 00:00:00');
    
    // These will throw an error
    decoder('20181215');
    decoder('Not a date');
    decoder('2018-1215');
  • literalOf - validates that a value is an exact match for a configured liteal value. Valid types can be boolean, number, or string.

    const decoder = Decode.literalOf(999);
    
    decoder(999); // yields 999
    decoder(888); // throws an error
  • number - converts a numeric string to a number.

    const decoder = Decode.number();
    
    decoder(999); // yields 999
    decoder('999'); // also yields 999
    decoder('not number'); // throws an error
  • object - converts an object to another object. Each property is mapped based on a configured decoder.

    const decoder = Decode.object({
      first: ['FIRST', Decode.string()],
      last: ['LAST', Decode.string()],
      age: ['AGE', Decode.number()]
    }); // (raw: any) => { first: string, last: string, age: number }
    
    // Yields { first: 'Jane', last: 'Doe', age: 33 }
    decoder({
      FIRST: 'Jane',
      LAST: 'Doe',
      AGE: 33
    });
    
    // Throws an error due to missing properties
    decoder({
      FIRST: 'Jane'
    });
  • string - stringifies boolean, number, and string values.

    const decoder = Decode.string();
    
    decoder(true); // yields 'true'
    decoder('test'); // yields 'test'
    decoder(4); // yields '4'
    
    // These will throw an Error
    decoder({});
    decoder([]);
    decoder(new Date());
  • type - pass-through decoder that only sets the type without changing the raw value. Useful for nominal typing scenarios.

    type ID = string & { __tag__: 'ID' }; // nominal type
    const idDecoder = type<ID>();
    
    idDecoder('999'); // typed as ID

Custom Decoders

Creating New Decoders

The createDecoder function can be used to create a new decoder. It takes an options object with the following properties:

  • errorMsg - function for creating an error message from the raw value if decoding fails.

  • isValid - function that determines whether a raw value is valid or not.

  • parse - transformation function that will be run on raw data to produce decoder output. The return type will also determine what the T type is of the Decode.Decoder<T> created by the function.

    // Configure a custom number decoder factory that only allows numbers as raw data
    const strictNumber = Decode.createDecoder({
      errorMsg: raw => Decode.errorFmt('Custom', 'a strict number', raw),
      isValid: raw => typeof raw === 'number',
      parse: raw => Number(raw)
    });
    
    const decoder: Decode.Decoder<number> = strictNumber();
    
    decoder(999); // yields 999
    decoder('999'); // throws an error
    
    const decoderWithDefault: Decode.Decoder<number | undefined> = strictNumber(undefined);
    
    decoderWithDefault(999); // yields 999
    decoderWithDefault('999'); // yields undefined

Combining Decoders

The pipe function can be used to create a new decoder that pipes data through multiple decoders.

// nominal type based on number
type ID = number & { __tag__: 'ID' };

const decoder = Decode.pipe(
  Decode.number(),
  Decode.type<ID>()
);

decoder('999'); // yields ID type with value 999

Releases

No releases published

Packages

No packages published