npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@karmaniverous/serify-deserify

v2.0.11

Published

Reversibly transform unserializable values into serializable ones. Includes Redux middleware.

Downloads

9,239

Readme

serify - reversibly transform an unserializable value into a serializable one

deserify - do the exact opposite

Why?

JSON.stringify and JSON.parse are a notoriously bad serializer/deserializer combination. They don't support important Javascript types like BigInt, Date, Map, Set, and unknown. Thanks to backward compatibility risk, they probably never will.

There are tons of custom serializers that address this issue, notably serialize-javascript and serializr. Unfortunately, some key Javascript tools like Redux explicitly depend on JSON.stringify & JSON.parse. So if you use Redux, none of those fancy serializers will help you get a Date or a BigInt into your store and back out again in one piece.

serify solves this problem by encoding those values (or structures containing them) into values that JSON.stringify can serialize without throwing an exception. After these values are retrieved and deserialized with JSON.parse, deserify returns them to their original state.

Usage

To install the package, run this command:

npm install @karmaniverous/serify-deserify

A simple example:

import {
  serify,
  deserify,
  defaultOptions,
} from '@karmaniverous/serify-deserify';

// A BigInt test value.
const value = 42n;

const serified = serify(value, defaultOptions);
// { serifyKey: null, type: 'BigInt', value: '42' }

const deserified = deserify(serified, defaultOptions);
// 42n

Review the unit tests for more examples of how to use serify and deserify.

See the createReduxMiddleware unit tests for a fully worked out example of how to configure & integrate the Redux middleware.

Serifiable Types

serify and deserify will work on values of any serifiable type.

A serifiable type is any type that is:

  • reversibly supported by JSON.stringify and JSON.parse, i.e. booleans, numbers, strings, plain objects, and arrays.
  • natively supported by serify, i.e. BigInt, Date, Map, Set, and unknown.
  • added to serify as a custom type.
  • composed exclusively of any of the above (e.g. an array of BigInt-keyed Maps of objects containing Sets of custom class instances).

serifyKey

serify works by converting unserializable values into structured objects that ARE serializable.

Consider the highly unlikely event that some data you want to deserify contains objects with exactly this form that were not produced by serify:

{
  serifyKey: null,
  type: 'Foo',
  value: 'Bar'
}

If you are using the default configuration (which does not support a Foo type), deserify will attempt to deserify this object and your process will either fail or produce an incorrect result.

In this case, simply add a non-null serifyKey of a serifiable primitive type (meaning a boolean, number, or string) to your options object, and everything will work again.

Options

Serifiable types and the serifyKey are defined in an options object, which specifies the logic that converts each type to and from a serializable form.

Default Configuration

Out of the box, the defaultOptions object supports the BigInt, Date, Map, Set, and unknown types.

If you only need the default configuration, simply import the defaultOptions object and pass it to serify and deserify:

import {
  serify,
  deserify,
  defaultOptions,
} from '@karmaniverous/serify-deserify';

// A BigInt test value.
const value = 42n;

const serified = serify(value, defaultOptions);
// { serifyKey: null, type: 'BigInt', value: '42' }

const deserified = deserify(serified, defaultOptions);
// 42n

Custom Configuration

If you need to change the serifyKey or add custom types, you can create a new options object and pass it to serify and deserify.

For a custom class that doesn't use a Static Type Property, the key of the related serify type is its class name. For anything else, the logic that determines the key is here.

import { serify, deserify, defaultOptions } from '@karmaniverous/serify-deserify';

// A custom class.
export class Custom {
  public p;

  constructor(p) {
    this.p = p;
  }
}

// A serify options object including support for the new custom type.
const customOptions = {
  ...defaultOptions
  serifyKey: 42,
  types: {
    ...defaultOptions.types,
    Custom: {
      serifier: (value) => value.p,
      deserifier: (value) => new Custom(value)
    }
  }
}

// A Custom test value.
const customAnswer = new Custom(42);

const serified = serify(customAnswer, customOptions);
// { serifyKey: 42, type: 'Custom', value: 42 }

const deserified = deserify(serified, customOptions);
// Custom { p: 42 }

Static Type Property

Normally, a type's key in the serify options object is the type's class name. If a class is dynamically generated, this value may not be known at compile time, so it would not be possible to configure it into the options object in a static manner.

One option is to alter the options object at runtime. Go nuts!

Another is to import the serifyStaticTypeProperty symbol to create a static property on your class. Use that as type key in your options object.

import {
  serify,
  deserify,
  defaultOptions,
  serifyStaticTypeProperty
} from '@karmaniverous/serify-deserify';

// A custom class.
export class CustomFoo {
  static [serifyStaticTypeProperty] = 'Foo';
  public p;

  constructor(p) {
    this.p = p;
  }
}

// A serify options object including support for the new custom type.
const customOptions = {
  ...defaultOptions
  serifyKey: 42,
  types: {
    ...defaultOptions.types,
    Foo: {
      serifier: (value) => value.p,
      deserifier: (value) => new CustomFoo(value)
    }
  }
}

// A Custom test value.
const customAnswer = new CustomFoo(42);

const serified = serify(customAnswer, customOptions);
// { serifyKey: 42, type: 'Foo', value: 42 }

const deserified = deserify(serified, customOptions);
// CustomFoo { p: 42 }

Typescript

serify-deserify is fully type-safe. If you are using TypeScript, you can define your custom types and options objects with full type checking.

This is accomplished by defining a special type map interface that maps a type's name to its types before and after serification. See defaultOptions.ts to review the default configuration as an example.

Here's the last example again, but with TypeScript:

import {
  serify,
  deserify,
  defaultOptions,
  serifyStaticTypeProperty,
  type SerifiableTypeMap,
  type SerifyOptions
} from '@karmaniverous/serify-deserify';

// A custom class.
export class CustomFoo {
  static [serifyStaticTypeProperty] = 'Foo';

  constructor(public p: number) {}
}

// Extend the default type map to include your new type.
// The tuple indicates the type before and after serification.
interface FooTypeMap extends SerifiableTypeMap {
  Foo: [CustomFoo, number]
}

// A serify options object including support for the new custom type.
const customOptions: SerifyOptions<CustomFooTypeMap> = {
  ...defaultOptions
  serifyKey: 42,
  types: {
    ...defaultOptions.types,
    Foo: {
      serifier: (value) => value.p,
      deserifier: (value) => new CustomFoo(value)
    }
  }
}

// A Custom test value.
const customAnswer = new CustomFoo(42);

const serified = serify(customAnswer, customOptions);
// { serifyKey: 42, type: 'Foo', value: 42 }

const deserified = deserify(serified, customOptions);
// CustomFoo { p: 42 }

Recursion

In the Custom Configuration example above, the Custom class contains a single property p that is populated with a primitive, serializable value (a number). So once a Custom value is serified with the serifier function defined above, there will be no difficulty serializing the value property of the resulting object.

What if the Custom class contained a property that was itself not serializable? This is the case with the Map class, which can contain keys and values of any type, including unserializable ones.

If you look at the defaultOptions object, you'll see that the Map type's serifier and deserfier functions are quite simple:

export interface DefaultTypeMap extends SerifiableTypeMap {
  ...;
  Map: [Map<unknown, unknown>, [unknown, unknown][]];
  ...;
}

export const defaultOptions: SerifyOptions<DefaultTypeMap> = {
  types: {
    ...,
    Map: {
      serifier: (value) => [...value.entries()],
      deserifier: (value) => new Map(value),
    },
    ...,
  },
};

This works because the serifier and deserifier functions are applied recursively. They only need to support the direct transformation of a type into a serializable form and back again, without regard to the resulting contents... so long as those contents are also composed of serifiable types.

Redux

The createReduxMiddleware function generates a Redux middleware that will serify every value pushed to your Redux store. If you use Redux Toolkit, leave the default serializeCheck middleware in place and it will notify you if you need to add a new type to your serify options!

When retrieving values from the Redux store, either deserify them explicitly or wrap your selectors in the deserify function.

See the createReduxMiddleware unit tests for a fully worked out example with custom types, or just try this for the out-of-the-box experience (H/T @tuffstuff9):

import {
  createReduxMiddleware,
  defaultOptions,
} from '@karmaniverous/serify-deserify';

// Create middleware.
const serifyMiddleware = createReduxMiddleware(defaultOptions);

// Construct slice.
const testSlice = createSlice({
  name: 'test',
  initialState,
  reducers: {
    setValue: (state, { payload }: PayloadAction<TestState['value']>) => {
      state.value = payload;
    },
  },
});

// Configure redux store.
const store = configureStore({
  reducer: combineReducers({
    test: testSlice.reducer,
  }),
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(serifyMiddleware),
});

Cloning in Deserify

deserify will not mutate the input value. It clones the value while recursively deserifying its contents.

It is implicitly assumed that the input value is composed entirely of serializable types, otherwise why bother attempting to deserify it?


See more great templates and other tools on my GitHub Profile!