Server

Serialization

Automatic serialization in mion using RunTypes.

mion uses @mionkit/run-types for automatic serialization. Dates, BigInts, Sets, Maps, and other complex types are automatically serialized and deserialized without any extra configuration.

import {Routes, route} from '@mionkit/router';
import {memoryStoreService} from './full-example.app.ts';

// Your TypeScript types ARE the validation schema
interface User {
    id: string;
    email: string;
    age: number;
    birthDate: Date;
    tags: Set<string>;
}

type NewUser = Omit<User, 'id'>;

// mion automatically:
// 1. Restores Date and Set from JSON
// 2. Validates user parameter
const routes = {
    createUser: route((ctx, user: NewUser): User => {
        // user is already validated and types are restored
        console.log(user.birthDate instanceof Date); // true
        console.log(user.tags instanceof Set); // true
        return memoryStoreService.createUser(user);
    }),
} satisfies Routes;
Serialization works transparently - you work with native TypeScript types and mion handles the JSON conversion automatically.

Serialization Modes

mion supports three serialization modes, each with different trade-offs for performance, data integrity, and use cases.

JSON Mode ('json')

The default JSON mode is optimized for speed. It only modifies properties that are not natively JSON-compatible (like Date, BigInt, Map, Set).

import {initRouter} from '@mionkit/router';

const router = initRouter({
    serialize: 'json', // default
});

Characteristics:

  • Fast: Minimal processing, only transforms non-JSON types and the uses native JSON.stringify/parse
  • Mutates objects: The original objects are modified in place for performance
  • Preserves unknown properties: Extra properties not in the type definition are kept

Best for:

  • When the Data is discarded after serialization (e.g., in a request/response cycle)
  • High-performance APIs where speed is critical
  • Internal services where you control both client and server
  • Cases where object mutation is acceptable

Stringify JSON Mode ('stringifyJson')

This mode creates a clean JSON output without mutating the original objects.

import {initRouter} from '@mionkit/router';

const router = initRouter({
    serialize: 'stringifyJson',
});

Characteristics:

  • Non-mutating: Original objects remain unchanged
  • Strips unknown properties: Only properties defined in the type are included
  • Clean output: Produces predictable, schema-compliant JSON

Best for:

  • Public APIs where you need strict schema compliance
  • Cases where you need to preserve original objects
  • Security-sensitive applications where stripping unknown data is important

Binary Mode ('binary')

Binary serialization provides the most compact data transfer, especially effective for numeric data.

import {initRouter} from '@mionkit/router';

const router = initRouter({
    serialize: 'binary',
});

Characteristics:

  • Compact: Significantly smaller payloads, especially for numeric data
  • Non-mutating: Original objects remain unchanged
  • Strips unknown properties: Only typed properties are serialized
  • Optimized for numbers: Using numeric type formats (NumInt32, NumUInt8, NumFloat) enables fixed-size encodings

Best for:

  • Large data transfers where bandwidth matters
  • Real-time applications requiring low latency
  • Mobile clients with limited bandwidth
  • APIs with complex nested data structures
  • Data-heavy applications with lots of numeric values

Comparison Table

FeaturejsonstringifyJsonbinary
SpeedFastestFastFast
Payload sizeLargerLargerSmallest
Mutates objectsYesNoNo
Strips unknown propsNoYesYes
Best forPerformanceClean APIsBandwidth

Why a Custom Binary Format?

You might wonder why mion uses its own binary protocol instead of established formats like Protocol Buffers, MessagePack, or CBOR. The answer lies in full TypeScript support and flexibility.

Full TypeScript Feature Support

Unlike Protocol Buffers which requires separate .proto schema files and has limited type support, mion's binary format is generated directly from your TypeScript types. This means it supports:

  • Union types - string | number | MyType
  • Discriminated unions - Tagged unions with type discriminators
  • Intersection types - TypeA & TypeB
  • Tuples - [string, number, boolean]
  • Optional properties - { name?: string }
  • Rest parameters - (...args: string[]) => void
  • Circular/recursive types - Types that reference themselves
  • Generics - Full generic type support
  • Enums - Both string and numeric enums
  • Maps and Sets - Native JavaScript collections
  • Classes - With custom serialization/deserialization

End-to-End Control

Since mion controls both the client and server serialization, we can optimize the protocol for our specific use case:

  • No schema files - Types are the schema
  • JIT compilation - Serializers are generated at runtime for maximum performance
  • Type-aware encoding - The encoder knows the exact type, eliminating type markers where possible
  • Compact representation - Optional properties use bitmaps, known properties don't need names
For a deep dive into how binary serialization works internally, see the Binary Serialization Deep Dive article.

Binary Serialization in Detail

For applications where data transfer efficiency is critical, binary serialization provides more compact data transfer compared to JSON.

Binary serialization is especially effective for transferring data structures that contain numeric data. Using the numeric type formats from @mionkit/type-formats (like NumInt32, NumUInt8, NumFloat) allows the binary serializer to use optimized fixed-size encodings, significantly reducing payload sizes compared to JSON.
import {PublicApi, Routes, initMionRouter, route} from '@mionkit/router';
import {NumInt32, NumUInt8, NumUInt16, NumFloat} from '@mionkit/type-formats/FormatsNumber';

/** Sensor reading with optimized numeric types for binary serialization */
export type SensorReading = {
    sensorId: NumUInt16; // 0-65535, uses 2 bytes in binary
    timestamp: NumInt32; // Unix timestamp, uses 4 bytes
    temperature: NumFloat; // Temperature reading
    humidity: NumUInt8; // 0-255%, uses 1 byte
    pressure: NumFloat; // Atmospheric pressure
};

/** Batch of sensor readings for efficient transfer */
export type SensorBatch = {
    batchId: NumUInt16;
    readings: SensorReading[];
};

/** Statistics result with numeric data */
export type SensorStats = {
    avgTemperature: NumFloat;
    avgHumidity: NumUInt8;
    avgPressure: NumFloat;
    readingCount: NumUInt16;
};

// Define routes with binary serialization
const routes = {
    /** Submit a single sensor reading */
    submitReading: route((_ctx, reading: SensorReading): {success: boolean; id: NumUInt16} => ({
        success: true,
        id: reading.sensorId,
    })),

    /** Submit a batch of sensor readings for efficient transfer */
    submitBatch: route((_ctx, batch: SensorBatch): {processed: NumUInt16} => ({
        processed: batch.readings.length as NumUInt16,
    })),

    /** Get statistics from sensor readings */
    getStats: route(
        (_ctx, sensorId: NumUInt16): SensorStats => ({
            avgTemperature: 22.5 as NumFloat,
            avgHumidity: 65 as NumUInt8,
            avgPressure: 1013.25 as NumFloat,
            readingCount: 100 as NumUInt16,
        })
    ),
} satisfies Routes;

// Initialize router with binary serialization globally
initMionRouter(routes, {serializer: 'binary'});
// Export API type for client usage
export type BinaryApi = PublicApi<typeof routes>;
Setting Binary Serialization Per Route

You can also enable binary serialization for specific routes by setting the serializer option:

const routes = {
    // JSON serialization (default)
    jsonRoute: route((ctx, data: MyType) => data),

    // Binary serialization for this route
    binaryRoute: route((ctx, data: MyType) => data, {serializer: 'binary'}),
} satisfies Routes;
Client Usage

The client automatically detects the serialization format and handles it transparently:

import {initClient} from '@mionkit/client';
import type {BinaryApi, SensorReading, SensorBatch} from './binary-server-example.ts';
import type {NumUInt16, NumUInt8, NumFloat, NumInt32} from '@mionkit/type-formats/FormatsNumber';

// Initialize client with the server URL
const {routes} = initClient<BinaryApi>({baseURL: 'http://localhost:3000'});

// Create a sensor reading with optimized numeric types
const reading: SensorReading = {
    sensorId: 1 as NumUInt16,
    timestamp: Math.floor(Date.now() / 1000) as NumInt32,
    temperature: 23.5 as NumFloat,
    humidity: 65 as NumUInt8,
    pressure: 1013.25 as NumFloat,
};

// Submit a single reading
const [result, error] = await routes.submitReading(reading).call();
console.log(result, error);
// Logs { success: true, id: 1 }

// Submit a batch of readings for efficient transfer
const batch: SensorBatch = {
    batchId: 1 as NumUInt16,
    readings: [reading, {...reading, sensorId: 2 as NumUInt16}],
};
const [batchResult] = await routes.submitBatch(batch).call();
console.log(batchResult);
// Logs { processed: 2 }

// Get statistics for a sensor
const [stats] = await routes.getStats(1 as NumUInt16).call();
console.log(stats);
// Logs { avgTemperature: 22.5, avgHumidity: 65, avgPressure: 1013.25, readingCount: 100 }