Define your types once, generate serializers for every language, and use them in your app.
The fastest way to use Sia is to skip the manual addString8 / readString8 calls entirely. Define your data structures in a .sia file, compile once, and import the generated code. You get type-safe encode/decode functions in every language — from a single source of truth.
This guide covers the schema-first workflow. If you prefer writing serialization code by hand (or only have one or two message types), see the manual Quick Start.
This is where the magic is — import and use the generated code just like any other module:
import { Sia } from "@timeleap/sia";
import { encodeUser, decodeUser, type User } from "./user";
// Create a user
const alice: User = {
name: "Alice",
age: 30,
email: "alice@example.com",
active: true,
};
// Encode to bytes — one line
const bytes = encodeUser(new Sia(), alice).toUint8Array();
// Decode from bytes — one line
const decoded = decodeUser(new Sia(bytes));
console.log(decoded.name); // "Alice"
console.log(decoded.email); // "alice@example.com"
package main
import (
"fmt"
sia "github.com/TimeleapLabs/go-sia/v2/pkg"
"./schema" // your generated file
)
func main() {
// Create a user
alice := schema.User{
Name: "Alice",
Age: 30,
Email: "alice@example.com",
Active: true,
}
// Encode to bytes
bytes := alice.Sia().Bytes()
// Decode from bytes
var decoded schema.User
decoded.FromSia(sia.NewFromBytes(bytes))
fmt.Println(decoded.Name) // "Alice"
fmt.Println(decoded.Email) // "alice@example.com"
}
from sia import Sia
from user import User # your generated file
# Create a user
alice = User(
name="Alice",
age=30,
email="alice@example.com",
active=True,
)
# Encode to bytes
s = Sia()
alice.encode(s)
raw_bytes = s.content
# Decode from bytes
reader = Sia(raw_bytes)
decoded = User.decode(reader)
print(decoded.name) # "Alice"
print(decoded.email) # "alice@example.com"
#include "user.hpp"
#include <iostream>
int main() {
// Create a user
User alice{"Alice", 30, "alice@example.com", true};
// Encode to bytes
auto encoded = encodeUser(alice);
auto bytes = encoded->Bytes();
// Decode from bytes
auto reader = sia::NewFromBytes(bytes);
User decoded = decodeUser(reader);
std::cout << decoded.name << std::endl; // "Alice"
std::cout << decoded.email << std::endl; // "alice@example.com"
}
Here's what writing the same User type looks like manually vs with the schema compiler:
Manual (you write this)
interface User {
name: string;
age: number;
email: string;
active: boolean;
}
function encodeUser(sia: Sia, user: User): Sia {
sia.addString8(user.name);
sia.addUInt8(user.age);
sia.addString16(user.email);
sia.addBool(user.active);
return sia;
}
function decodeUser(sia: Sia): User {
return {
name: sia.readString8(),
age: sia.readUInt8(),
email: sia.readString16(),
active: sia.readBool(),
};
}
type User struct {
Name string
Age uint8
Email string
Active bool
}
func EncodeUser(s *sia.Sia, u User) {
s.AddString8(u.Name)
s.AddUInt8(u.Age)
s.AddString16(u.Email)
s.AddBool(u.Active)
}
func DecodeUser(s *sia.Sia) User {
return User{
Name: s.ReadString8(),
Age: s.ReadUInt8(),
Email: s.ReadString16(),
Active: s.ReadBool(),
}
}
class User:
def __init__(self, name, age, email, active):
self.name = name
self.age = age
self.email = email
self.active = active
def encode(self, sia):
sia.add_string8(self.name)
sia.add_uint8(self.age)
sia.add_string16(self.email)
sia.add_bool(self.active)
return sia
@classmethod
def decode(cls, sia):
return cls(
name=sia.read_string8(),
age=sia.read_uint8(),
email=sia.read_string16(),
active=sia.read_bool(),
)
struct User {
std::string name;
uint8_t age;
std::string email;
bool active;
};
std::shared_ptr<sia::Sia> encodeUser(User u) {
auto s = sia::New();
s->AddString8(u.name);
s->AddUInt8(u.age);
s->AddString16(u.email);
s->AddBool(u.active);
return s;
}
User decodeUser(std::shared_ptr<sia::Sia> s) {
User u;
u.name = s->ReadString8();
u.age = s->ReadUInt8();
u.email = s->ReadString16();
u.active = s->ReadBool();
return u;
}
Schema (the compiler writes it for you)
schema User {
name string8
age uint8
email string16
active bool
}
Run sia compile user.sia -o user.ts and you're done.
Run sia compile user.sia -o user.go and you're done.
Run sia compile user.sia -o user.py and you're done.
Run sia compile user.sia -o user.cpp and you're done.
The manual version is 15-25 lines of careful, order-dependent code per type. The schema version is 6 lines. Add ten more message types and the difference is enormous.