Skip to content

kpietraszko/schemind

Repository files navigation

schemind

NPM Version Code Coverage Brotli Size

Read and write to messages serialized as arrays (aka indexed keys messages) by defining a schema. Protocol‑agnostic.

What?

In formats like JSON, a message normally looks something like this:

{
  "id": 1,
  "fullName": "John Doe",
  "email": "johndoe@example.com",
  "birthDate": "1973-01-22",
  "address": {
    "street": "123 Main Street",
    "city": "Anytown",
    "zipcode": "12345-6789",
    "geo": {
      "lat": 42.1234,
      "lng": -71.2345
    }
  },
  "website": "www.johndoe.com"
}

I'm using JSON as an example here, but schemind is essentially protocol-agnostic. I use it with MessagePack.

If you desperately need to make this message more compact, you could alternatively serialize it like so:

[
  1,
  "John Doe",
  "johndoe@example.com",
  "1973-01-22",
  [
    "123 Main Street",
    "Anytown",
    "12345-6789",
    [
      42.1234,
      -71.2345
    ]
  ],
  "www.johndoe.com"
]

This is sometimes referred to as a message with indexed keys.

Schemind helps you create and read such messages, if your (de)serializer doesn't support this technique.

Note that this format obviously has some drawbacks: recommended reading about the pros and cons.

Installation

npm install schemind

Usage

Defining a schema

import { buildSchema, withIndex as i } from "schemind";

const personSchema = buildSchema({
  id: i(0)<number>(),
  fullName: i(1)<string>(),
  email: i(2)<string>(),
  birthDate: i(3)<Date>(),
  address: i(4)({
    street: i(0)<string>(),
    city: i(1)<string>(),
    zipcode: i(2)<string>(),
    geo: i(3)({
      lat: i(0)<number>(),
      lng: i(1)<number>()
    })
  }),
  website: i(5)<string>()
});

Every field needs to have its index in the message specified using withIndex.
Note that this also goes for nested objects, such as address.

  • If you accidentally pass the same index twice, or you forget to call withIndex on any nested object, buildSchema will throw an InvalidSchemaError.
  • If you forget to call buildSchema on your object, you'll get a type error when trying to use your schema.

Reading from a message

Say you have an incoming message (from network/storage/whatever) like this:

const incomingMessage = JSON.parse(`
[
  1,
  "John Doe",
  "johndoe@example.com",
  "1973-01-22",
  [
    "123 Main Street",
    "Anytown",
    "12345-6789",
    [
      42.1234,
      -71.2345
    ]
  ],
  "www.johndoe.com"
]`);

There are 2 ways to read this message:

toPlainObject

This is the more convenient option.

import { toPlainObject } from "schemind";

const messageAsObject = toPlainObject(incomingMessage, personSchema);
//    ^ this has the following type:
//   {
//     id: number,
//     fullName: string,
//     email: string,
//     birthDate: Date,
//     address: {
//       street: string,
//       city: string,
//       zipcode: string,
//       geo: {
//         lat: number,
//         lng: number
//       }
//     },
//     website: string
//   }

get

This is the more performant option – it doesn't allocate on the heap.

const fullName = get(incomingMessage, personSchema.fullName);
//    ^ this is of type string

const latitude = get(incomingMessage, personSchema.address.geo.lat);
//    ^ this is of type number

Alternatively, you can use the method "get". It works in the exact same way.

const fullName = personSchema.fullName.get(incomingMessage);
const latitude = personSchema.address.geo.lat.get(incomingMessage);

Writing

There are 2 ways to write a message.

toIndexedKeysMessage

import { toIndexedKeysMessage } from "schemind";

const objectToSerialize = {
  id: 1,
  fullName: "John Doe",
  email: "johndoe@example.com",
  birthDate: new Date(),
  address: {
    street: "123 Main Street",
    city: "Anytown",
    zipcode: "12345-6789",
    geo: {
      lat: 42.1234,
      lng: -71.2345
    }
  },
  website: "www.johndoe.com"
};

const message = toIndexedKeysMessage(objectToSerialize, personSchema);
//    ^ this is an array that's the same as the "incomingMessage" in the previous section

// JSON.stringify(message) or whatever

set

import { set } from "schemind";

const newMessage: unknown[] = [];
set(newMessage, personSchema.fullName, "John Doe");
set(newMessage, personSchema.address.geo.lat, 42.1234);
//                                            ^ this is type-checked
// etc

Alternatively, you can use the method "set". It works in the exact same way.

personSchema.fullName.set(newMessage, "John Doe");
personSchema.address.geo.lat.set(newMessage, 42.1234);

FAQ

Shouldn't this be an extension of a serializer?

Probably.

Wouldn't it be better to use protobuf at this point?

Possibly. But if you're already using JSON / MessagePack / CBOR etc. in your app, and you need more compact messages for some features — schemind could be useful.

Additionally, in some languages (backend or frontend) there's a MessagePack or JSON implementation that's faster, or allocates less memory, than protobuf.

Why would I use get if it's inconvenient?

The get function prioritizes performance over convenience. The main goal here is to avoid any heap allocations (beyond what your deserializer allocates). I use schemind in performance-critical scenarios, where avoiding GC pauses is crucial.
Use the toPlainObject function instead, if you don't mind some extra allocations.

Related work

About

No description or website provided.

Topics

Resources

License

Stars

Watchers

Forks

Contributors 2

  •  
  •