No description
Find a file
2025-05-25 11:06:06 -05:00
dist clean up interfaces; merge nextSeed/next methods in accessors 2025-05-25 10:54:38 -05:00
src clean up interfaces; merge nextSeed/next methods in accessors 2025-05-25 10:54:38 -05:00
.gitignore update exports 2025-05-18 17:59:38 -05:00
package-lock.json use symbol.metadata 2025-05-18 21:42:39 -05:00
package.json prepare to add options 2025-05-22 03:35:53 -05:00
README.md update readme 2025-05-25 11:06:06 -05:00
tsconfig.json update tsconfig 2025-05-18 20:37:00 -05:00

serde-ts

a library for serializing and deserializing javascript objects

Usage

this library makes no assumptions about formats and loosely follows the architecture of Rust's serde crate. the major difference are the particular data types. serde-ts's internal data model contains the following types

serde-ts data types

  • null/undefined
  • boolean
  • number
  • bigint
  • string
  • symbol
  • function
  • object
  • iterable
  • class

with the exception of iterables and classes, these types were chosen as they are the ones provided by Javascript's native typeof operator which is used internally for type checking.

there are five interfaces which are relevant to serde-ts users.

Serializer

interface ISerializer<T> {
  serializeAny?(value: any): T
  serializeBoolean(value: boolean): T
  serializeNumber(value: number): T
  serializeBigInt(value: bigint): T
  serializeString(value: string): T
  serializeSymbol(value: symbol): T
  serializeNull(): T
  serializeIterable(len?: number): ISerializeIterable<T>
  serializeObject(len?: number): ISerializeObject<T>
  serializeClass(name: string, len?: number): ISerializeObject<T>
}

a Serializer is responsible for transforming the internal serde-ts data model into the target serialization format. this means that it will have a serialize<Type> function for each of serde-ts's data types.

Deserializer

interface IDeserializer {
  deserializeAny<T>(visitor: Partial<IVisitor<T>>): T
  deserializeBoolean<T>(visitor: Partial<IVisitor<T>>): T
  deserializeNumber<T>(visitor: Partial<IVisitor<T>>): T
  deserializeBigInt<T>(visitor: Partial<IVisitor<T>>): T
  deserializeString<T>(visitor: Partial<IVisitor<T>>): T
  deserializeSymbol<T>(visitor: Partial<IVisitor<T>>): T
  deserializeNull<T>(visitor: Partial<IVisitor<T>>): T
  deserializeObject<T>(visitor: Partial<IVisitor<T>>): T
  deserializeIterable<T>(visitor: Partial<IVisitor<T>>): T
  deserializeFunction<T>(visitor: Partial<IVisitor<T>>): T
}

a Deserializer is responsible for transforming the target serialization format into the internal serde-ts data model. it will have a deserialize<Type> for each of the serde-ts's data types.

Serialize

interface Serialize<T, U> {
  (serializer: ISerializer<T>, value: U): T
}

Serialize is responsible for transforming a value to the internal data model. for example, a Vector2 may be internally serialized as an iterable of numbers.

Deserialize

interface Deserialize<T> {
  (deserializer: IDeserializer): T
}

Deserialize is responsible for transforming a value from the internal data model. it is the inverse of Serialize. depending on the Deserialize target, it may accept an iterable of numbers and produce a Vector2.

Visitor

interface IVisitor<T> {
  visitBoolean(value: boolean): T
  visitNumber(value: number): T
  visitBigInt(value: bigint): T
  visitString(value: string): T
  visitSymbol(value: symbol): T
  visitNull(): T
  visitObject(access: IMapAccess): T
  visitIterable(access: IIterableAccess): T
}

a Visitor is part of the deserialization process and is how each data type is mapped from the internal data model to whatever the end structure is. Deserialize is responsible for creating a Visitor and giving it to a Deserializer. that Visitor will then be given every value once it has been deserialized into serde-ts's internal model.

Example

in simple cases, a formatter for serde-ts will expose a pair of functions like toString and fromString which will handle serializing and deserializing for you.

in more complex cases, you still won't be implementing Serializer/Deserializer, only consuming them with Serialize/Deserialize. assuming we're using a format which reads/writes JSON

import { registerSerialize, registerDeserialize } from 'serde'
import { ISerializer } from 'serde/ser'
import { forward, IDeserializer, IIterableAccess, IMapAccess } from 'serde/de'
import { fromString, toString } from 'serde-json-ts'

class Vector2 {
  x: number
  y: number

  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }

  static serialize(serializer: ISerializer<string>, value: Vector2) {
    const iter = serializer.serializeIterable() // returns an ISerializeIterable<void>
    iter.serializeElement(value.x)
    iter.serializeElement(value.y)
    return iter.end()
  }

  static deserialize(deserializer: IDeserializer) {
    return deserializer.deserializeIterable({
      // we could implement visitNumber here, but we'll let the default
      // deserializer handle it
      visitIterable(access: IIterableAccess) {
        const elements = []

        for (const item of access) {
          elements.push(item as number)
        }

        return new Vector2(elements[0], elements[1])
      }
    })
  }
}

class Entity {
  name: string
  position: Vector2

  constructor(name: string, position: Vector2) {
    this.name = name
    this.position = position
  }

  static serialize(serializer: ISerializer<string>, value: Entity) {
    const ser = serializer.serializeObject()
    ser.serializeEntry('name', value.name)
    ser.serializeEntry('position', value.position)
    return ser.end()
  }

  static deserialize(deserializer: IDeserializer) {
    return deserializer.deserializeObject({
      visitObject(access: IMapAccess) {
        let name, position
        for (const [key, value] of access) {
          switch (key) {
            case 'name':
              name = value
              break
            case 'position':
              // forward the deserialization to Vector2
              position = forward(value as string, Vector2)
              break
          }
        }

        return new Entity(name as string, position as Vector2)
      }
    })
  }
}

// we're registering to the global serde registry
registerSerialize(Vector2, Vector2.serialize)
registerDeserialize(Vector2, Vector2.deserialize)
registerSerialize(Entity, Entity.serialize)
registerDeserialize(Entity, Entity.deserialize)

const one = new Vector2(1, 1)
const serialized = toString(one)
console.log(serialized)
// "[1, 1]"

const deserializedOne = fromString(serialized, Vector2)
console.log(deserializedOne)
// Vector2 { x: 1, y: 1 }

const player = new Entity('Player', one)
const serializedPlayer = toString(player)
console.log(serializedPlayer)
// {"name":"Player","position":[1,1]}

const deserializedPlayer = fromString(serializedPlayer, Entity)
console.log(deserializedPlayer)
// Entity { name: 'Player', position: Vector2 { x: 1, y: 1 } }