Serialization & Compression
Control how data is serialized and compressed in cache layers for optimal performance and storage efficiency.
Table of Contents
JSON Serializer
Default serializer for all cache layers. Uses JSON.stringify and JSON.parse with prototype pollution protection.
Usage
import { JsonSerializer } from 'layercache'
const serializer = new JsonSerializer()
// Serialize
const payload = serializer.serialize({ foo: 'bar' })
// '{"foo":"bar"}'
// Deserialize
const data = serializer.deserialize('{"foo":"bar"}')
// { foo: 'bar' }
Built-In Usage
JSON serialization is used by default:
import { MemoryLayer, RedisLayer, DiskLayer } from 'layercache'
// All use JsonSerializer by default
const memory = new MemoryLayer({ ttl: 60_000 })
const redis = new RedisLayer({ client: redis, ttl: 300_000 })
const disk = new DiskLayer({ directory: './cache' })
// Explicitly specify JSON serializer
const redisJson = new RedisLayer({
client: redis,
ttl: 300_000,
serializer: new JsonSerializer()
})
Handling Special Values
const serializer = new JsonSerializer()
// Dates become ISO strings
serializer.serialize({ date: new Date('2024-04-11') })
// '{"date":"2024-04-11T00:00:00.000Z"}'
// undefined becomes null (or omitted)
serializer.serialize({ foo: undefined })
// '{"foo":null}' or '{}'
// Functions are removed
serializer.serialize({ foo: () => {} })
// '{}'
Prototype Pollution Protection
All deserialized data is sanitized to prevent prototype pollution attacks:
const serializer = new JsonSerializer()
// Attempted prototype pollution is blocked
const data = serializer.deserialize('{"__proto__":{"polluted":true}}')
// -> Sanitized to: {}
// -> Object.prototype.polluted remains undefined
MessagePack Serializer
Binary serialization format that's more compact and faster than JSON.
Installation
npm install @msgpack/msgpack
Usage
import { MsgpackSerializer } from 'layercache'
const serializer = new MsgpackSerializer()
// Serialize
const payload = serializer.serialize({ foo: 'bar', num: 42 })
// Buffer <81 a3 66 6f 6f a3 62 61 72 a3 6e 75 6d 2a>
// Deserialize
const data = serializer.deserialize(payload)
// { foo: 'bar', num: 42 }
Use with Cache Layers
import { RedisLayer, DiskLayer } from 'layercache'
const redis = new RedisLayer({
client: redis,
ttl: 300_000,
serializer: new MsgpackSerializer()
})
const disk = new DiskLayer({
directory: './cache',
serializer: new MsgpackSerializer()
})
Benefits
- Smaller size: ~30-50% smaller than JSON for typical data
- Faster: Binary parsing is faster than JSON
- Type preservation: Preserves binary data, dates, etc.
Comparison
import { JsonSerializer, MsgpackSerializer } from 'layercache'
const data = {
id: 123,
name: 'Alice',
email: 'alice@example.com',
tags: ['user', 'active'],
created: new Date()
}
const jsonSerializer = new JsonSerializer()
const msgpackSerializer = new MsgpackSerializer()
const jsonPayload = jsonSerializer.serialize(data)
// '{"id":123,"name":"Alice","email":"alice@example.com","tags":["user","active"],"created":"2024-04-11T00:00:00.000Z"}'
// Length: ~140 bytes
const msgpackPayload = msgpackSerializer.serialize(data)
// Binary buffer
// Length: ~95 bytes (32% smaller)
console.log(`JSON size: ${jsonPayload.length} bytes`)
console.log(`MessagePack size: ${msgpackPayload.length} bytes`)
Custom Serializers
Implement the CacheSerializer interface to create custom serialization logic.
Interface
interface CacheSerializer {
serialize(value: unknown): string | Buffer
deserialize<T>(payload: string | Buffer): T
}
Example: CBOR Serializer
import { encode, decode } from 'cbor'
class CborSerializer implements CacheSerializer {
serialize(value: unknown): Buffer {
return Buffer.from(encode(value))
}
deserialize<T>(payload: string | Buffer): T {
const buffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, 'binary')
return decode(buffer) as T
}
}
// Usage
const redis = new RedisLayer({
client: redis,
ttl: 300_000,
serializer: new CborSerializer()
})
Example: Base64 JSON Serializer
class Base64JsonSerializer implements CacheSerializer {
serialize(value: unknown): string {
const json = JSON.stringify(value)
return Buffer.from(json).toString('base64')
}
deserialize<T>(payload: string | Buffer): T {
const normalized = typeof payload === 'string' ? payload : payload.toString('binary')
const json = Buffer.from(normalized, 'base64').toString('utf8')
return JSON.parse(json) as T
}
}
// Usage
const memcached = new MemcachedLayer({
client: memcached,
ttl: 300_000,
serializer: new Base64JsonSerializer()
})
Example: Compressing Serializer
import { gzip, ungzip } from 'node:zlib'
import { promisify } from 'node:util'
const gzipAsync = promisify(gzip)
const ungzipAsync = promisify(ungzip)
class GzipJsonSerializer implements CacheSerializer {
async serialize(value: unknown): Promise<Buffer> {
const json = JSON.stringify(value)
return await gzipAsync(Buffer.from(json))
}
async deserialize<T>(payload: string | Buffer): Promise<T> {
const buffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload)
const decompressed = await ungzipAsync(buffer)
return JSON.parse(decompressed.toString('utf8')) as T
}
// Note: This returns Promise<Buffer> but interface expects sync
// For async serialization, use a wrapper layer
}
Prototype Pollution Protection
All built-in serializers protect against prototype pollution attacks by sanitizing deserialized data.
What is Prototype Pollution?
Prototype pollution is a vulnerability where attackers modify Object.prototype to affect all objects in the application:
{
"__proto__": {
"admin": true
}
}
Without protection, this could make every object have admin: true.
How layercache Protects
layercache uses StructuredDataSanitizer to:
- Block dangerous keys: Removes
__proto__, constructor, prototype
- Limit depth: Prevents deeply nested objects (default: 200 levels)
- Limit nodes: Prevents excessive object size (default: 10,000 nodes)
Configuration
import { JsonSerializer } from 'layercache'
const serializer = new JsonSerializer()
// Default limits
const data = serializer.deserialize(payload)
// -> maxDepth: 200
// -> maxNodes: 10,000
// Custom limits (not exposed in API, defaults are safe)
// JsonSerializer uses built-in safe defaults
Example: Blocked Attack
const serializer = new JsonSerializer()
const maliciousPayload = JSON.stringify({
user: 'alice',
__proto__:: { admin: true }
})
const data = serializer.deserialize(maliciousPayload)
// -> { user: 'alice' }
// -> __proto__ is removed
// -> Object.prototype.admin remains undefined
console.log(({} as any).admin) // undefined (safe!)
MessagePack Protection
MessagePack serializer also sanitizes data:
import { MsgpackSerializer } from 'layercache'
const serializer = new MsgpackSerializer()
// MessagePack payloads are sanitized on deserialize
const data = serializer.deserialize(maliciousMsgpackBuffer)
// -> Dangerous keys removed
// -> Depth and size limits enforced
Compression
Reduce memory usage and network bandwidth by compressing cached values. Supported in RedisLayer.
Gzip Compression
import { RedisLayer } from 'layercache'
const redis = new RedisLayer({
client: redis,
ttl: 300_000,
compression: 'gzip',
compressionThreshold: 1_024 // Only compress values >1KB
})
// Large values are compressed automatically
await cache.set('large-data', largeObject)
// -> Serialized to JSON (or MessagePack)
// -> Compressed with gzip if >1KB
// -> Stored in Redis
// Decompression is automatic on read
const data = await cache.get('large-data')
// -> Fetched from Redis
// -> Decompressed
// -> Deserialized
Brotli Compression
const redis = new RedisLayer({
client: redis,
ttl: 300_000,
compression: 'brotli',
compressionThreshold: 512 // Compress values >512 bytes
})
Compression Threshold
Skip compression for small values (overhead isn't worth it):
const redis = new RedisLayer({
client: redis,
ttl: 300_000,
compression: 'gzip',
compressionThreshold: 1_024 // 1KB threshold
})
// Small values: not compressed
await cache.set('small', { id: 1 })
// Serialized: ~15 bytes
// Stored: ~15 bytes (not compressed)
// Large values: compressed
await cache.set('large', largeArray)
// Serialized: ~10KB
// Stored: ~3KB (compressed)
Decompression Max Bytes
Prevent decompression bomb attacks:
const redis = new RedisLayer({
client: redis,
ttl: 300_000,
compression: 'gzip',
compressionThreshold: 1_024,
decompressionMaxBytes: 64 * 1_024 * 1_024 // 64MiB limit
})
// If decompressed data exceeds 64MiB, read fails and key is deleted
const data = await cache.get('malicious-key')
// -> Throws error
// -> Key is deleted from Redis
Compressed values use a custom header format:
LCZ1:<algorithm>:<compressed-data>
Examples:
LCZ1:gzip:<gzip-data>
LCZ1:brotli:<brotli-data>
This header allows:
- Format detection on read
- Future algorithm support
- Backward compatibility
Compression tradeoffs:
Recommendations:
// Use gzip for most cases (balanced)
const redis = new RedisLayer({
client: redis,
compression: 'gzip',
compressionThreshold: 1_024
})
// Use brotli for storage-constrained environments
const redis = new RedisLayer({
client: redis,
compression: 'brotli',
compressionThreshold: 512
})
// Disable compression for low-latency requirements
const redis = new RedisLayer({
client: redis,
compression: undefined // No compression
})
Serializer Chain
Try multiple deserializers in sequence for smooth data format migrations.
Configuration
import { RedisLayer, JsonSerializer, MsgpackSerializer } from 'layercache'
const redis = new RedisLayer({
client: redis,
ttl: 300_000,
serializer: [
new MsgpackSerializer(), // Try MessagePack first
new JsonSerializer() // Fall back to JSON
]
})
How It Works
On write: Always use the first serializer
await cache.set('user:123', userData)
// Serialized with MsgpackSerializer (first in array)
On read: Try each serializer until one succeeds
const data = await cache.get('user:123')
// Try MsgpackSerializer.deserialize()
// -> If success: return data
// -> If error: try JsonSerializer.deserialize()
// -> If success: return data, migrate to MessagePack
// -> If error: delete key and return null
Auto-Migration
When a value is successfully deserialized with a non-primary serializer, it's automatically rewritten with the primary serializer:
// Old data in Redis: JSON format
const oldData = '{"id":123,"name":"Alice"}'
// Read with serializer chain
const data = await cache.get('user:123')
// -> Try MessagePack: fails
// -> Try JSON: succeeds, returns { id: 123, name: 'Alice' }
// -> Rewrites with MessagePack
// -> Next read is faster (MessagePack is first)
Migration Example
// Phase 1: Deploy with JSON only
const redis = new RedisLayer({
client: redis,
serializer: new JsonSerializer()
})
// Phase 2: Deploy with MessagePack, keep JSON fallback
const redis = new RedisLayer({
client: redis,
serializer: [
new MsgpackSerializer(), // New format
new JsonSerializer() // Old format
]
})
// Phase 3: Data auto-migrates on access
// -> Existing JSON keys work fine
// -> On first read, rewritten as MessagePack
// -> New keys written as MessagePack
// Phase 4: Remove JSON fallback (after all data migrated)
const redis = new RedisLayer({
client: redis,
serializer: new MsgpackSerializer()
})
Complex Chain
import {
MsgpackSerializer,
JsonSerializer,
CborSerializer
} from 'layercache'
const redis = new RedisLayer({
client: redis,
serializer: [
new MsgpackSerializer(), // Current format
new CborSerializer(), // Previous format
new JsonSerializer() // Legacy format
]
})
// Tries formats in order:
// 1. MessagePack (current)
// 2. CBOR (previous)
// 3. JSON (legacy)
// 4. Delete if all fail
Best Practices
1. Use MessagePack for Large Data
// GOOD: MessagePack for large datasets
const cache = new RedisLayer({
client: redis,
serializer: new MsgpackSerializer(),
compression: 'gzip',
compressionThreshold: 1_024
})
2. Enable Compression for Redis
// GOOD: Reduce Redis memory usage
const redis = new RedisLayer({
client: redis,
compression: 'gzip',
compressionThreshold: 1_024
})
3. Use Serializer Chain for Migrations
// GOOD: Support multiple formats during migration
const redis = new RedisLayer({
client: redis,
serializer: [
new MsgpackSerializer(), // New
new JsonSerializer() // Old
]
})
4. Set Compression Threshold Appropriately
// GOOD: Avoid compressing small values
const redis = new RedisLayer({
client: redis,
compression: 'gzip',
compressionThreshold: 1_024 // Only compress >1KB
})
5. Protect Against Decompression Bombs
// GOOD: Set decompression limit
const redis = new RedisLayer({
client: redis,
compression: 'gzip',
decompressionMaxBytes: 64 * 1_024 * 1_024 // 64MiB
})
6. Don't Compress Already-Compressed Data
// BAD: Compressing already compressed data
const cache = new RedisLayer({
client: redis,
serializer: new MsgpackSerializer(), // Binary
compression: 'gzip' // Compressing binary
})
// GOOD: Skip compression for binary data
const cache = new RedisLayer({
client: redis,
serializer: new MsgpackSerializer() // No compression needed
})