A production-ready TypeScript serialization/deserialization library inspired by Rust's serde. Provides pure structural transformation between types and their serialized forms with Result-based error handling.
- 🦀 Rust-inspired: API design inspired by Rust's powerful serde library with Result types
- 🔧 Pure transformation: Focus on serialization/deserialization with type-safe error handling
- 🌳 Tree-shakeable: ESM-only with minimal bundle size
- 🎯 Type-safe: Full TypeScript support with excellent type inference
- 🚫 Zero dependencies: Lightweight with only @rustify/result as dependency
- 🔄 Composable: Build complex serializers from simple building blocks
- 📦 Browser-first: Designed for modern browsers and bundlers
- ⚡ Fast: Optimized for performance
# npm
npm install @rustify/serde
# pnpm
pnpm add @rustify/serde
# yarn
yarn add @rustify/serde
import * as t from '@rustify/serde'
// Define a person serializer
const PersonSerde = t.object({
name: t.string,
age: t.number,
active: t.boolean
})
// Serialize data (returns Result<S, string>)
const person = { name: "Alice", age: 30, active: true }
const serializedResult = PersonSerde.serialize(person)
if (serializedResult.isOk()) {
console.log(serializedResult.value) // { name: "Alice", age: 30, active: true }
}
// Deserialize data (returns Result<T, string>)
const result = PersonSerde.deserialize(serializedResult.value)
if (result.isOk()) {
console.log(result.value) // { name: "Alice", age: 30, active: true }
} else {
console.error(result.error) // Error message
}
// Use .unwrap() when you're confident the operation will succeed
const serialized = PersonSerde.serialize(person).unwrap()
const deserialized = PersonSerde.deserialize(serialized).unwrap()
All serializers implement the Serde<T, S>
interface:
interface Serde<T, S> {
serialize(value: T): Result<S, string>
deserialize(serialized: unknown): Result<T, string>
}
Both serialization and deserialization operations return Result<T, string>
for type-safe error handling:
import * as t from '@rustify/serde'
const result = t.string.deserialize(123) // not a string
if (result.isOk()) {
console.log(result.value) // string
} else {
console.log(result.error) // "Expected string, got number"
}
// Use .unwrap() when you want throwing behavior
try {
const value = t.string.deserialize(123).unwrap()
} catch (error) {
console.error(error.message) // "Expected string, got number"
}
import * as t from '@rustify/serde'
// Basic types (all return Result<S, string>)
const stringResult = t.string.serialize("hello")
if (stringResult.isOk()) {
console.log(stringResult.value) // "hello"
}
const numberResult = t.number.serialize(42)
if (numberResult.isOk()) {
console.log(numberResult.value) // 42
}
const boolResult = t.boolean.serialize(true)
if (boolResult.isOk()) {
console.log(boolResult.value) // true
}
// Date serialization (to/from ISO string)
const date = new Date("2023-01-01T00:00:00.000Z")
const dateResult = t.date.serialize(date)
if (dateResult.isOk()) {
console.log(dateResult.value) // "2023-01-01T00:00:00.000Z"
}
const dateResult = t.date.deserialize("2023-01-01T00:00:00.000Z")
if (dateResult.isOk()) {
console.log(dateResult.value) // Date object
}
import * as t from '@rustify/serde'
const ConstantSerde = t.literal("CONSTANT")
const literalResult = ConstantSerde.serialize("CONSTANT")
if (literalResult.isOk()) {
console.log(literalResult.value) // "CONSTANT"
}
const result = ConstantSerde.deserialize("CONSTANT")
if (result.isOk()) {
console.log(result.value) // "CONSTANT"
}
import * as t from '@rustify/serde'
const PersonSerde = t.object({
name: t.string,
age: t.number
})
import * as t from '@rustify/serde'
const NumberArraySerde = t.array(t.number)
const arrayResult = NumberArraySerde.serialize([1, 2, 3])
if (arrayResult.isOk()) {
console.log(arrayResult.value) // [1, 2, 3]
}
import * as t from '@rustify/serde'
const CoordinateSerde = t.tuple(t.number, t.number, t.string)
const tupleResult = CoordinateSerde.serialize([10, 20, "point"])
if (tupleResult.isOk()) {
console.log(tupleResult.value) // [10, 20, "point"]
}
import * as t from '@rustify/serde'
const StringRecordSerde = t.record(t.string)
const recordResult = StringRecordSerde.serialize({ key: "value" })
if (recordResult.isOk()) {
console.log(recordResult.value) // { key: "value" }
}
import * as t from '@rustify/serde'
const OptionalString = t.optional(t.string)
const optionalResult1 = OptionalString.serialize(undefined)
if (optionalResult1.isOk()) {
console.log(optionalResult1.value) // undefined
}
const optionalResult2 = OptionalString.serialize("hello")
if (optionalResult2.isOk()) {
console.log(optionalResult2.value) // "hello"
}
import * as t from '@rustify/serde'
const NullableString = t.nullable(t.string)
const nullableResult1 = NullableString.serialize(null)
if (nullableResult1.isOk()) {
console.log(nullableResult1.value) // null
}
const nullableResult2 = NullableString.serialize("hello")
if (nullableResult2.isOk()) {
console.log(nullableResult2.value) // "hello"
}
import * as t from '@rustify/serde'
const NumberWithDefault = t.withDefault(t.number, 0)
NumberWithDefault.deserialize(undefined) // 0
NumberWithDefault.deserialize(42) // 42
Transform data during serialization/deserialization:
import { createTransformSerde } from '@rustify/serde/serializers/primitives'
import * as t from '@rustify/serde'
import { Ok, Err } from '@rustify/result'
// Boolean to "True"/"False" string transformation
const BooleanString = createTransformSerde(
t.string,
(value: boolean) => value ? "True" : "False",
(serialized: string) => serialized === "True",
(serialized: string) => {
if (serialized === "True") return Ok(true)
if (serialized === "False") return Ok(false)
return Err(`Invalid boolean string: ${serialized}`)
}
)
const serializeResult1 = BooleanString.serialize(true)
if (serializeResult1.isOk()) {
console.log(serializeResult1.value) // "True"
}
const serializeResult2 = BooleanString.serialize(false)
if (serializeResult2.isOk()) {
console.log(serializeResult2.value) // "False"
}
const result1 = BooleanString.deserialize("True")
if (result1.isOk()) {
console.log(result1.value) // true
}
const result2 = BooleanString.deserialize("Invalid")
if (result2.isErr()) {
console.log(result2.error) // "Invalid boolean string: Invalid"
}
Handle recursive data structures using getter methods (similar to Zod's approach):
import * as t from '@rustify/serde'
import type { Serde } from '@rustify/serde'
interface TreeNode {
value: number
name: string
children?: TreeNode[]
}
// Use getters to define self-referential types
const TreeNodeSerde = t.object({
value: t.number,
name: t.string,
get children() {
return t.optional(t.array(TreeNodeSerde))
}
}) as Serde<TreeNode, Record<string, unknown>>
// Now you can serialize/deserialize tree structures
const tree: TreeNode = {
value: 1,
name: "root",
children: [
{ value: 2, name: "child1" },
{ value: 3, name: "child2", children: [
{ value: 4, name: "grandchild" }
]}
]
}
const serializedResult = TreeNodeSerde.serialize(tree)
if (serializedResult.isErr()) {
console.error("Serialization failed:", serializedResult.error)
return
}
const serialized = serializedResult.value
const result = TreeNodeSerde.deserialize(serialized)
if (result.isOk()) {
const deserialized = result.value
console.log(deserialized)
}
You can also represent mutually recursive types using getters:
import * as t from '@rustify/serde'
import type { Serde } from '@rustify/serde'
interface User {
email: string
posts: Post[]
}
interface Post {
title: string
author: User
}
const UserSerde = t.object({
email: t.string,
get posts() {
return t.array(PostSerde)
}
}) as Serde<User, Record<string, unknown>>
const PostSerde = t.object({
title: t.string,
get author() {
return UserSerde
}
}) as Serde<Post, Record<string, unknown>>
// Note: Be careful with cyclical data - it will cause infinite loops
All deserialization operations return Result<T, string>
for comprehensive error handling:
import * as t from '@rustify/serde'
const PersonSerde = t.object({
name: t.string,
age: t.number
})
const result = PersonSerde.deserialize({
name: "Alice",
age: "not a number" // Invalid!
})
if (result.isOk()) {
console.log(result.value) // Person
} else {
console.log(result.error) // "Field 'age': Expected number, got string"
}
// Chain operations with Result methods
const chainedResult = t.number.deserialize(42)
.map(n => n * 2)
.map(n => `The result is ${n}`)
if (chainedResult.isOk()) {
console.log(chainedResult.value) // "The result is 84"
}
For convenience, all serializers are available on the default export:
import * as t from '@rustify/serde'
const PersonSerde = t.object({
name: t.string,
age: t.number,
active: t.boolean
})
import * as t from '@rustify/serde'
// Create a union of different types
const StringOrNumberSerde = t.union([t.string, t.number])
const result1 = StringOrNumberSerde.deserialize("hello")
if (result1.isOk()) {
console.log(result1.value) // "hello"
}
const result2 = StringOrNumberSerde.deserialize(42)
if (result2.isOk()) {
console.log(result2.value) // 42
}
const result3 = StringOrNumberSerde.deserialize(true)
if (result3.isErr()) {
console.log(result3.error) // Union deserialization error
}
import * as t from '@rustify/serde'
const UserSerde = t.object({
id: t.number,
profile: t.object({
name: t.string,
email: t.optional(t.string),
preferences: t.record(t.boolean)
}),
posts: t.array(t.object({
title: t.string,
content: t.string,
tags: t.array(t.string)
}))
})
// Handle complex nested deserialization
const userData = {
id: 1,
profile: {
name: "Alice",
email: "[email protected]",
preferences: { darkMode: true, notifications: false }
},
posts: [
{ title: "Hello", content: "World", tags: ["intro", "greeting"] }
]
}
const result = UserSerde.deserialize(userData)
if (result.isOk()) {
console.log("Valid user data:", result.value)
} else {
console.log("Validation error:", result.error)
}
@rustify/serde targets modern browsers that support:
- ES2022 features
- ESM modules
- Node.js 22+ LTS
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Run
pnpm test
andpnpm build
- Submit a pull request
# Install dependencies
pnpm install
# Run tests
pnpm test
# Run tests in watch mode
pnpm test:watch
# Run with coverage
pnpm test:coverage
# Build the package
pnpm build
# Lint code
pnpm lint
# Format code
pnpm format
# Run examples
pnpm examples
# Run individual examples
pnpm examples:basic
pnpm examples:recursive
MIT © pavi2410
This library is inspired by Rust's serde library, adapted for TypeScript's type system and JavaScript ecosystem.