Define Binary Protocols Once, Generate Everywhere

BinSchema turns declarative, bit-precise schemas into production-ready encoders, decoders, and documentation for TypeScript, Go, and Rust.

Overview

Bit-Level Precision

Define fields from 1 to 64 bits with configurable bit ordering (MSB/LSB first). Perfect for network protocols and hardware formats.

Rich Type System

Primitives, strings, arrays, discriminated unions, back-references, and variable-length integers (LEB128, VLQ, DER).

Computed Fields

Auto-calculate lengths, positions, element counts, and CRC32 checksums. No manual bookkeeping needed.

Multi-Language Output

Generate type-safe encoders and decoders for TypeScript, Go, and Rust from a single schema definition.

IDE Support

JSON Schema available for autocomplete and validation in VS Code, WebStorm, and any editor with JSON Schema support.

Documentation Generation

Generate HTML documentation directly from your schema. Keep docs and code in sync automatically.

When to Use BinSchema

BinSchema is a schema-first tool for designing compact binary protocols. Define your wire format once, get type-safe encoders and decoders in every target language.

Use BinSchema when…

  • You're building a real-time protocol (games, chat, telemetry) over WebSockets or TCP and every byte counts
  • You need both encoding and decoding — client and server, possibly in different languages
  • Your format uses bit-packed flags, variable-length fields, computed lengths, or CRC checksums
  • You want one schema, multiple languages — TypeScript frontend, Go backend, Rust embedded

How it compares

  • vs. Protobuf / FlatBuffers / Cap'n Proto — those impose their own wire format. BinSchema gives you full control over every bit on the wire
  • vs. Kaitai Struct — Kaitai is decode-only. BinSchema generates both encoders and decoders from the same schema
  • vs. hand-written parsers — BinSchema eliminates the boilerplate and keeps all languages in sync automatically

Performance

DNS packet decode benchmark — same packets, same schema, all languages. The C parser is hand-written and serves as the theoretical floor.

DNS Query (29 bytes)

ImplementationDecodeEncode
C hand-written32 ns
Rust BinSchema136 ns186 ns
ldns (C library)214 ns431 ns
Go BinSchema (optimized)96 ns
Go BinSchema (standard)320 ns1,048 ns
Go Kaitai Struct395 ns
TS BinSchema995 ns950 ns

DNS Response (45 bytes)

ImplementationDecodeEncode
C hand-written35 ns
Rust BinSchema225 ns309 ns
ldns (C library)325 ns520 ns
Go BinSchema (optimized)165 ns
Go BinSchema (standard)500 ns1,511 ns
Go Kaitai Struct610 ns
TS BinSchema1,600 ns1,500 ns

Kaitai Struct and the C parser are decode-only. ldns is a production C DNS library (NLnet Labs). Rust encode uses encode_into with a reused encoder. All benchmarks on Apple M3 Max.

Installation

From npm

npm install binschema

Usage

# Generate TypeScript code
binschema generate --language ts --schema protocol.bschema.json --out ./generated

# Generate Go code
binschema generate --language go --schema protocol.bschema.json --out ./generated

# Generate Rust code
binschema generate --language rust --schema protocol.bschema.json --out ./generated

# Generate HTML documentation
binschema docs build --schema protocol.bschema.json --out docs.html

See It in Action

Define your schema once, generate type-safe encoders and decoders for TypeScript, Go, and Rust.

1. Define Your Schema

{
  "config": { "endianness": "big_endian" },
  "types": {
    "SensorReading": {
      "sequence": [
        { "name": "device_id",   "type": "uint16"  },
        { "name": "temperature", "type": "float32" },
        { "name": "humidity",    "type": "uint8"   },
        { "name": "timestamp",   "type": "uint32"  }
      ]
    }
  }
}

2. Generate Code

binschema generate --language ts --schema sensor.schema.json --out ./generated
// Generated types and encoder/decoder classes

export interface SensorReadingInput {
  device_id: number;    // uint16
  temperature: number;  // float32
  humidity: number;     // uint8
  timestamp: number;    // uint32
}

export interface SensorReadingOutput {
  device_id: number;
  temperature: number;
  humidity: number;
  timestamp: number;
}

export class SensorReadingEncoder extends BitStreamEncoder {
  encode(value: SensorReadingInput): Uint8Array { /* ... */ }
}

export class SensorReadingDecoder extends SeekableBitStreamDecoder {
  decode(): SensorReadingOutput { /* ... */ }
}
View full generated code
import { BitStreamEncoder, Endianness } from "./bit-stream.js";
import { SeekableBitStreamDecoder } from "./seekable-bit-stream.js";
import { createReader } from "./binary-reader.js";

export interface SensorReadingInput {
  /** 16-bit Unsigned Integer */
  device_id: number;
  /** 32-bit Floating Point (IEEE 754) */
  temperature: number;
  /** 8-bit Unsigned Integer */
  humidity: number;
  /** 32-bit Unsigned Integer */
  timestamp: number;
}

export interface SensorReadingOutput {
  device_id: number;
  temperature: number;
  humidity: number;
  timestamp: number;
}

export type SensorReading = SensorReadingOutput;

export class SensorReadingEncoder extends BitStreamEncoder {
  constructor() {
    super("msb_first");
  }

  encode(value: SensorReadingInput): Uint8Array {
    this.writeUint16(value.device_id, "big_endian");
    this.writeFloat32(value.temperature, "big_endian");
    this.writeUint8(value.humidity);
    this.writeUint32(value.timestamp, "big_endian");
    return this.finish();
  }

  calculateSize(value: SensorReading): number {
    let size = 0;
    size += 2; // device_id
    size += 4; // temperature
    size += 1; // humidity
    size += 4; // timestamp
    return size;
  }
}

export class SensorReadingDecoder extends SeekableBitStreamDecoder {
  constructor(input: Uint8Array | number[] | string) {
    const reader = createReader(input);
    super(reader, "msb_first");
  }

  decode(): SensorReadingOutput {
    return {
      device_id: this.readUint16("big_endian"),
      temperature: this.readFloat32("big_endian"),
      humidity: this.readUint8(),
      timestamp: this.readUint32("big_endian"),
    };
  }
}

3. Use the Generated Code

import { SensorReadingEncoder, SensorReadingDecoder } from "./generated.js";

// Encode a sensor reading to binary
const encoder = new SensorReadingEncoder();
const bytes = encoder.encode({
  device_id: 42,
  temperature: 23.5,
  humidity: 65,
  timestamp: 1700000000,
});
// bytes: Uint8Array(11) — ready to send over the wire

// Decode binary back to a typed object
const decoder = new SensorReadingDecoder(bytes);
const reading = decoder.decode();
console.log(reading.temperature); // 23.5
console.log(reading.device_id);   // 42

Schema Definition Reference

A quick overview of the types and constructs available in a BinSchema definition.

Primitives

  • uint8, uint16, uint32, uint64
  • int8, int16, int32, int64
  • float32, float64
  • bool

Bit-Level

  • bit — 1–64 bits, MSB or LSB first
  • Mixed bit and byte fields in the same struct
  • Configurable bit ordering per-schema or per-field

Strings & Bytes

  • Fixed-length and length-prefixed strings
  • Null-terminated strings
  • Raw byte arrays with field-referenced lengths

Arrays

  • Fixed count, field-referenced count
  • Length-delimited (byte length from another field)
  • Variant-terminated (read until a discriminator matches)

Computed Fields

  • length_of — byte length of another field
  • count_of — element count of an array
  • position_of — byte offset for random access
  • crc32_of — CRC32 checksum

Advanced

  • Discriminated unions (tagged variants)
  • Conditional fields (if expressions)
  • Variable-length integers (LEB128, VLQ, DER)
  • Constant and reserved fields

Full Type Reference

Examples

Explore real-world schemas and the documentation generated directly from them.