Skip to content

Repository files navigation

@rustify/serde

CInpm version

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.

Features

  • 🦀 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

Installation

# npm
npm install @rustify/serde
# pnpm
pnpm add @rustify/serde
# yarn
yarn add @rustify/serde

Quick Start

import*astfrom'@rustify/serde'// Define a person serializerconstPersonSerde=t.object({name: t.string,age: t.number,active: t.boolean})// Serialize data (returns Result<S, string>)constperson={name: "Alice",age: 30,active: true}constserializedResult=PersonSerde.serialize(person)if(serializedResult.isOk()){console.log(serializedResult.value)// { name: "Alice", age: 30, active: true }}// Deserialize data (returns Result<T, string>)constresult=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 succeedconstserialized=PersonSerde.serialize(person).unwrap()constdeserialized=PersonSerde.deserialize(serialized).unwrap()

Core Concepts

Serde Interface

All serializers implement the Serde<T, S> interface:

interfaceSerde<T,S>{serialize(value: T): Result<S,string>deserialize(serialized: unknown): Result<T,string>}

Result-Based Error Handling

Both serialization and deserialization operations return Result<T, string> for type-safe error handling:

import*astfrom'@rustify/serde'constresult=t.string.deserialize(123)// not a stringif(result.isOk()){console.log(result.value)// string}else{console.log(result.error)// "Expected string, got number"}// Use .unwrap() when you want throwing behaviortry{constvalue=t.string.deserialize(123).unwrap()}catch(error){console.error(error.message)// "Expected string, got number"}

API Reference

Primitive Serializers

import*astfrom'@rustify/serde'// Basic types (all return Result<S, string>)conststringResult=t.string.serialize("hello")if(stringResult.isOk()){console.log(stringResult.value)// "hello"}constnumberResult=t.number.serialize(42)if(numberResult.isOk()){console.log(numberResult.value)// 42}constboolResult=t.boolean.serialize(true)if(boolResult.isOk()){console.log(boolResult.value)// true}// Date serialization (to/from ISO string)constdate=newDate("2023-01-01T00:00:00.000Z")constdateResult=t.date.serialize(date)if(dateResult.isOk()){console.log(dateResult.value)// "2023-01-01T00:00:00.000Z"}constdateResult=t.date.deserialize("2023-01-01T00:00:00.000Z")if(dateResult.isOk()){console.log(dateResult.value)// Date object}

Literal Values

import*astfrom'@rustify/serde'constConstantSerde=t.literal("CONSTANT")constliteralResult=ConstantSerde.serialize("CONSTANT")if(literalResult.isOk()){console.log(literalResult.value)// "CONSTANT"}constresult=ConstantSerde.deserialize("CONSTANT")if(result.isOk()){console.log(result.value)// "CONSTANT"}

Complex Types

Objects

import*astfrom'@rustify/serde'constPersonSerde=t.object({name: t.string,age: t.number})

Arrays

import*astfrom'@rustify/serde'constNumberArraySerde=t.array(t.number)constarrayResult=NumberArraySerde.serialize([1,2,3])if(arrayResult.isOk()){console.log(arrayResult.value)// [1, 2, 3]}

Tuples

import*astfrom'@rustify/serde'constCoordinateSerde=t.tuple(t.number,t.number,t.string)consttupleResult=CoordinateSerde.serialize([10,20,"point"])if(tupleResult.isOk()){console.log(tupleResult.value)// [10, 20, "point"]}

Records

import*astfrom'@rustify/serde'constStringRecordSerde=t.record(t.string)constrecordResult=StringRecordSerde.serialize({key: "value"})if(recordResult.isOk()){console.log(recordResult.value)// { key: "value" }}

Modifiers

Optional Fields

import*astfrom'@rustify/serde'constOptionalString=t.optional(t.string)constoptionalResult1=OptionalString.serialize(undefined)if(optionalResult1.isOk()){console.log(optionalResult1.value)// undefined}constoptionalResult2=OptionalString.serialize("hello")if(optionalResult2.isOk()){console.log(optionalResult2.value)// "hello"}

Nullable Fields

import*astfrom'@rustify/serde'constNullableString=t.nullable(t.string)constnullableResult1=NullableString.serialize(null)if(nullableResult1.isOk()){console.log(nullableResult1.value)// null}constnullableResult2=NullableString.serialize("hello")if(nullableResult2.isOk()){console.log(nullableResult2.value)// "hello"}

Default Values

import*astfrom'@rustify/serde'constNumberWithDefault=t.withDefault(t.number,0)NumberWithDefault.deserialize(undefined)// 0NumberWithDefault.deserialize(42)// 42

Custom Transformations

Transform data during serialization/deserialization:

import{createTransformSerde}from'@rustify/serde/serializers/primitives'import*astfrom'@rustify/serde'import{Ok,Err}from'@rustify/result'// Boolean to "True"/"False" string transformationconstBooleanString=createTransformSerde(t.string,(value: boolean)=>value ? "True" : "False",(serialized: string)=>serialized==="True",(serialized: string)=>{if(serialized==="True")returnOk(true)if(serialized==="False")returnOk(false)returnErr(`Invalid boolean string: ${serialized}`)})constserializeResult1=BooleanString.serialize(true)if(serializeResult1.isOk()){console.log(serializeResult1.value)// "True"}constserializeResult2=BooleanString.serialize(false)if(serializeResult2.isOk()){console.log(serializeResult2.value)// "False"}constresult1=BooleanString.deserialize("True")if(result1.isOk()){console.log(result1.value)// true}constresult2=BooleanString.deserialize("Invalid")if(result2.isErr()){console.log(result2.error)// "Invalid boolean string: Invalid"}

Recursive Types

Handle recursive data structures using getter methods (similar to Zod's approach):

import*astfrom'@rustify/serde'importtype{Serde}from'@rustify/serde'interfaceTreeNode{value: numbername: stringchildren?: TreeNode[]}// Use getters to define self-referential typesconstTreeNodeSerde=t.object({value: t.number,name: t.string,getchildren(){returnt.optional(t.array(TreeNodeSerde))}})asSerde<TreeNode,Record<string,unknown>>// Now you can serialize/deserialize tree structuresconsttree: TreeNode={value: 1,name: "root",children: [{value: 2,name: "child1"},{value: 3,name: "child2",children: [{value: 4,name: "grandchild"}]}]}constserializedResult=TreeNodeSerde.serialize(tree)if(serializedResult.isErr()){console.error("Serialization failed:",serializedResult.error)return}constserialized=serializedResult.valueconstresult=TreeNodeSerde.deserialize(serialized)if(result.isOk()){constdeserialized=result.valueconsole.log(deserialized)}

Mutually Recursive Types

You can also represent mutually recursive types using getters:

import*astfrom'@rustify/serde'importtype{Serde}from'@rustify/serde'interfaceUser{email: stringposts: Post[]}interfacePost{title: stringauthor: User}constUserSerde=t.object({email: t.string,getposts(){returnt.array(PostSerde)}})asSerde<User,Record<string,unknown>>constPostSerde=t.object({title: t.string,getauthor(){returnUserSerde}})asSerde<Post,Record<string,unknown>>// Note: Be careful with cyclical data - it will cause infinite loops

Error Handling

All deserialization operations return Result<T, string> for comprehensive error handling:

import*astfrom'@rustify/serde'constPersonSerde=t.object({name: t.string,age: t.number})constresult=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 methodsconstchainedResult=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"}

Default Export

For convenience, all serializers are available on the default export:

import*astfrom'@rustify/serde'constPersonSerde=t.object({name: t.string,age: t.number,active: t.boolean})

Advanced Usage

Union Types and Error Handling

import*astfrom'@rustify/serde'// Create a union of different typesconstStringOrNumberSerde=t.union([t.string,t.number])constresult1=StringOrNumberSerde.deserialize("hello")if(result1.isOk()){console.log(result1.value)// "hello"}constresult2=StringOrNumberSerde.deserialize(42)if(result2.isOk()){console.log(result2.value)// 42}constresult3=StringOrNumberSerde.deserialize(true)if(result3.isErr()){console.log(result3.error)// Union deserialization error}

Working with Complex Nested Data

import*astfrom'@rustify/serde'constUserSerde=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 deserializationconstuserData={id: 1,profile: {name: "Alice",email: "alice@example.com",preferences: {darkMode: true,notifications: false}},posts: [{title: "Hello",content: "World",tags: ["intro","greeting"]}]}constresult=UserSerde.deserialize(userData)if(result.isOk()){console.log("Valid user data:",result.value)}else{console.log("Validation error:",result.error)}

Browser Support

@rustify/serde targets modern browsers that support:

  • ES2022 features
  • ESM modules
  • Node.js 22+ LTS

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests for new functionality
  5. Run pnpm test and pnpm build
  6. Submit a pull request

Development

# 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

License

MIT © pavi2410

Inspiration

This library is inspired by Rust's serde library, adapted for TypeScript's type system and JavaScript ecosystem.

About

A production-ready TypeScript serialization/deserialization library inspired by Rust's serde.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages