serde-ts/README.md
2025-05-25 11:06:06 -05:00

203 lines
6.5 KiB
Markdown

# 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](https://serde.rs/) 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`
```ts
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`
```ts
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`
```ts
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`
```ts
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`
```ts
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
```ts
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 } }
```