dist | ||
src | ||
.gitignore | ||
package-lock.json | ||
package.json | ||
README.md | ||
tsconfig.json |
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 } }