Serialization
mion uses @mionjs/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 '@mionjs/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 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 '@mionjs/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 '@mionjs/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 '@mionjs/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 (
FormatInt32,FormatUInt8,FormatFloat) 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
| Feature | json | stringifyJson | binary |
|---|---|---|---|
| Speed | Fastest | Fast | Fast |
| Payload size | Larger | Larger | Smallest |
| Mutates objects | Yes | No | No |
| Strips unknown props | No | Yes | Yes |
| Best for | Performance | Clean APIs | Bandwidth |
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
Binary Serialization in Detail
For applications where data transfer efficiency is critical, binary serialization provides more compact data transfer compared to JSON.
@mionjs/type-formats (like FormatInt32, FormatUInt8, FormatFloat) allows the binary serializer to use optimized fixed-size encodings, significantly reducing payload sizes compared to JSON.import {PublicApi, Routes, initMionRouter, route} from '@mionjs/router';
import {FormatInt32, FormatUInt8, FormatUInt16, FormatFloat} from '@mionjs/type-formats/NumberFormats';
/** Sensor reading with optimized numeric types for binary serialization */
export type SensorReading = {
sensorId: FormatUInt16; // 0-65535, uses 2 bytes in binary
timestamp: FormatInt32; // Unix timestamp, uses 4 bytes
temperature: FormatFloat; // Temperature reading
humidity: FormatUInt8; // 0-255%, uses 1 byte
pressure: FormatFloat; // Atmospheric pressure
};
/** Batch of sensor readings for efficient transfer */
export type SensorBatch = {
batchId: FormatUInt16;
readings: SensorReading[];
};
/** Statistics result with numeric data */
export type SensorStats = {
avgTemperature: FormatFloat;
avgHumidity: FormatUInt8;
avgPressure: FormatFloat;
readingCount: FormatUInt16;
};
// Define routes with binary serialization
const routes = {
/** Submit a single sensor reading */
submitReading: route((_ctx, reading: SensorReading): {success: boolean; id: FormatUInt16} => ({
success: true,
id: reading.sensorId,
})),
/** Submit a batch of sensor readings for efficient transfer */
submitBatch: route((_ctx, batch: SensorBatch): {processed: FormatUInt16} => ({
processed: batch.readings.length as FormatUInt16,
})),
/** Get statistics from sensor readings */
getStats: route(
(_ctx, sensorId: FormatUInt16): SensorStats => ({
avgTemperature: 22.5 as FormatFloat,
avgHumidity: 65 as FormatUInt8,
avgPressure: 1013.25 as FormatFloat,
readingCount: 100 as FormatUInt16,
})
),
} 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 '@mionjs/client';
import type {BinaryApi, SensorReading, SensorBatch} from './binary-server-example.ts';
import type {FormatUInt16, FormatUInt8, FormatFloat, FormatInt32} from '@mionjs/type-formats/NumberFormats';
// 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 FormatUInt16,
timestamp: Math.floor(Date.now() / 1000) as FormatInt32,
temperature: 23.5 as FormatFloat,
humidity: 65 as FormatUInt8,
pressure: 1013.25 as FormatFloat,
};
// 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 FormatUInt16,
readings: [reading, {...reading, sensorId: 2 as FormatUInt16}],
};
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 FormatUInt16).call();
console.log(stats);
// Logs { avgTemperature: 22.5, avgHumidity: 65, avgPressure: 1013.25, readingCount: 100 }