No description
Find a file
2025-05-25 10:55:30 -05:00
dist clean up interfaces; merge nextSeed/next methods in accessors 2025-05-25 10:54:38 -05:00
src get rid of unnecessary class 2025-05-25 10:55:30 -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 correct default implementation of MapAccess.entries; add length params to Serializer object methods 2025-05-24 22:59:34 -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 { IDeserializer, IIterableAccess } 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
  }
}

// we're registering to the global serde registry
registerSerialize(Vector2, (serializer: ISerializer<void>, value: Vector2) => {
  const iter = serializer.serializeIterable() // returns an ISerializeIterable<void>
  iter.serializeElement(value.x)
  iter.serializeElement(value.y)
  return iter.end()
})

registerDeserialize(Vector2, (deserializer: IDeserializer) => 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])
  }
})
)

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 }