SuperJSON
シリアライゼーションライブラリ
SuperJSON
概要
SuperJSONは、JavaScriptの式を安全にJSONのスーパーセットにシリアライズするライブラリです。Date、BigInt、Map、Set、RegExpなど、標準JSONでは対応できないJavaScriptの型を処理し、循環参照や参照等価性も保持します。Next.jsなどのフルスタックフレームワークでのデータ送信に特に有効です。
詳細
SuperJSONは、標準JSONの制限を克服するために設計されたライブラリです。JSON.stringify()とJSON.parse()の薄いラッパーとして機能し、JavaScriptのあらゆる値をシリアライズできます。特にサーバーサイドレンダリングやフルスタックアプリケーションにおいて、複雑なデータ構造を安全に送信するために開発されました。
主な特徴:
- 拡張型サポート: Date、RegExp、Map、Set、BigInt、undefined、Symbol
- 参照等価性: 循環参照と参照等価性の保持
- 高忠実度: あらゆるJavaScript値の完全なシリアライズ
- JSON互換: 標準JSONと完全互換のオブジェクト出力
- 2段階API: 高レベルAPIと低レベルAPIの選択
技術的詳細:
- stringify/parse: 高レベルAPI、JSON.stringify/parseの代替
- serialize/deserialize: 低レベルAPI、jsonとmetaの分離
- メタデータ: 元の型情報の保持とデシリアライズ時の復元
- カスタム変換: 独自データ型の登録と変換
- Next.js統合: getServerSidePropsなどでの使用に最適化
対応データ型:
- プリミティブ型(標準JSON対応)
- Date、RegExp、Map、Set、BigInt
- undefined、Symbol
- 循環参照のあるオブジェクト
- カスタム登録型
メリット・デメリット
メリット
- 標準JSONでは扱えない豊富な型をサポート
- 循環参照や参照等価性の完全な保持
- Next.jsなどフルスタックフレームワークとの優れた統合
- 既存のJSONワークフローとの互換性
- TypeScriptの完全サポート
- 軽量で高速な処理
デメリット
- メタデータによるサイズの増加
- 標準JSONツールとの直接的な互換性がない
- 学習コストが存在する
- Node.js v16以降が必要
- デバッグ時の可読性が低下する場合がある
参考ページ
- GitHubリポジトリ: https://github.com/flightcontrolhq/superjson
- ドキュメント: https://github.com/flightcontrolhq/superjson/blob/main/README.md
- NPMパッケージ: https://www.npmjs.com/package/superjson
- Next.js統合ガイド: https://nextjs.org/docs/api-reference/data-fetching/get-server-side-props
書き方の例
基本的な使用方法
import superjson from 'superjson';
interface User {
id: number;
name: string;
email: string;
createdAt: Date;
lastLogin?: Date;
}
const user: User = {
id: 1,
name: 'Alice',
email: '[email protected]',
createdAt: new Date('2023-01-01'),
lastLogin: new Date('2023-01-15')
};
// SuperJSONでシリアライズ
const serialized = superjson.stringify(user);
console.log('Serialized:', serialized);
// SuperJSONでデシリアライズ
const deserialized = superjson.parse(serialized) as User;
console.log('Deserialized:', deserialized);
console.log('createdAt is Date?', deserialized.createdAt instanceof Date);
低レベルAPIの使用
import superjson from 'superjson';
const complexData = {
id: 1,
created: new Date(),
pattern: /hello/gi,
bigNumber: BigInt('12345678901234567890'),
optional: undefined,
settings: new Map([
['theme', 'dark'],
['language', 'en']
])
};
// serialize()で低レベルAPIを使用
const { json, meta } = superjson.serialize(complexData);
console.log('JSON part:', json);
console.log('Meta part:', meta);
// deserialize()で復元
const deserialized = superjson.deserialize({ json, meta });
console.log('Deserialized:', deserialized);
console.log('Map restored?', deserialized.settings instanceof Map);
循環参照の処理
import superjson from 'superjson';
interface Node {
id: number;
name: string;
children: Node[];
parent?: Node;
}
// 循環参照を持つデータ構造
const root: Node = {
id: 1,
name: 'root',
children: []
};
const child1: Node = {
id: 2,
name: 'child1',
children: [],
parent: root
};
const child2: Node = {
id: 3,
name: 'child2',
children: [],
parent: root
};
root.children = [child1, child2];
// 循環参照があってもシリアライズ可能
const serialized = superjson.stringify(root);
console.log('Serialized with circular refs');
// 循環参照を保持してデシリアライズ
const deserialized = superjson.parse(serialized) as Node;
console.log('Deserialized:', deserialized.name);
console.log('Child parent is root?', deserialized.children[0].parent === deserialized);
Map、Set、BigIntの使用
import superjson from 'superjson';
interface AdvancedData {
id: number;
tags: Set<string>;
metadata: Map<string, any>;
largeNumber: BigInt;
coefficients: number[];
}
const data: AdvancedData = {
id: 1,
tags: new Set(['typescript', 'javascript', 'node']),
metadata: new Map([
['version', '1.0.0'],
['author', 'Alice'],
['created', new Date()]
]),
largeNumber: BigInt('999999999999999999999'),
coefficients: [1.5, 2.7, 3.14159]
};
// 高度な型をシリアライズ
const serialized = superjson.stringify(data);
console.log('Serialized advanced data');
// 高度な型をデシリアライズ
const deserialized = superjson.parse(serialized) as AdvancedData;
console.log('Tags:', Array.from(deserialized.tags));
console.log('Metadata version:', deserialized.metadata.get('version'));
console.log('Large number:', deserialized.largeNumber.toString());
console.log('Set instance?', deserialized.tags instanceof Set);
console.log('Map instance?', deserialized.metadata instanceof Map);
Next.jsでの使用
import superjson from 'superjson';
import { GetServerSideProps } from 'next';
interface PageProps {
user: {
id: number;
name: string;
createdAt: Date;
preferences: Map<string, any>;
};
stats: {
views: BigInt;
lastUpdated: Date;
};
}
// サーバーサイドでのデータ取得
export const getServerSideProps: GetServerSideProps<PageProps> = async () => {
const user = {
id: 1,
name: 'Alice',
createdAt: new Date('2023-01-01'),
preferences: new Map([
['theme', 'dark'],
['notifications', true]
])
};
const stats = {
views: BigInt('1234567890'),
lastUpdated: new Date()
};
// SuperJSONでシリアライズしてクライアントに送信
return {
props: superjson.parse(superjson.stringify({ user, stats }))
};
};
// クライアントサイドでの使用
const MyPage: React.FC<PageProps> = ({ user, stats }) => {
return (
<div>
<h1>Welcome, {user.name}!</h1>
<p>Member since: {user.createdAt.toLocaleDateString()}</p>
<p>Theme: {user.preferences.get('theme')}</p>
<p>Total views: {stats.views.toString()}</p>
</div>
);
};
カスタム型の登録
import superjson from 'superjson';
// カスタムクラス
class Point {
constructor(public x: number, public y: number) {}
distance(other: Point): number {
return Math.sqrt((this.x - other.x) ** 2 + (this.y - other.y) ** 2);
}
}
// カスタム型を登録
superjson.registerCustom<Point, [number, number]>(
{
isApplicable: (v): v is Point => v instanceof Point,
serialize: (v) => [v.x, v.y],
deserialize: (v) => new Point(v[0], v[1])
},
'Point'
);
const point1 = new Point(0, 0);
const point2 = new Point(3, 4);
const data = { point1, point2 };
// カスタム型を含むデータをシリアライズ
const serialized = superjson.stringify(data);
console.log('Serialized custom types');
// カスタム型を含むデータをデシリアライズ
const deserialized = superjson.parse(serialized) as typeof data;
console.log('Distance:', deserialized.point1.distance(deserialized.point2));
console.log('Point instance?', deserialized.point1 instanceof Point);
エラーハンドリング
import superjson from 'superjson';
interface SafeData {
id: number;
name: string;
created: Date;
}
function safeStringify<T>(obj: T): string | null {
try {
return superjson.stringify(obj);
} catch (error) {
console.error('SuperJSON stringify error:', error);
return null;
}
}
function safeParse<T>(str: string): T | null {
try {
return superjson.parse(str) as T;
} catch (error) {
console.error('SuperJSON parse error:', error);
return null;
}
}
// 正常なケース
const validData: SafeData = {
id: 1,
name: 'Alice',
created: new Date()
};
const serialized = safeStringify(validData);
if (serialized) {
const deserialized = safeParse<SafeData>(serialized);
console.log('Safe operation result:', deserialized);
}
// 無効なケース
const invalidJson = '{"invalid": json}';
const result = safeParse<SafeData>(invalidJson);
console.log('Invalid parse result:', result);
パフォーマンス比較
import superjson from 'superjson';
interface BenchmarkData {
id: number;
created: Date;
values: number[];
metadata: Map<string, any>;
}
function benchmarkSerialization(data: BenchmarkData[], iterations: number): void {
// SuperJSONでのテスト
const superJsonStart = Date.now();
for (let i = 0; i < iterations; i++) {
const serialized = superjson.stringify(data);
const deserialized = superjson.parse(serialized);
}
const superJsonTime = Date.now() - superJsonStart;
// 標準JSONでのテスト(比較用、Dateは文字列に変換)
const jsonData = data.map(item => ({
...item,
created: item.created.toISOString(),
metadata: Object.fromEntries(item.metadata)
}));
const jsonStart = Date.now();
for (let i = 0; i < iterations; i++) {
const serialized = JSON.stringify(jsonData);
const deserialized = JSON.parse(serialized);
}
const jsonTime = Date.now() - jsonStart;
console.log(`SuperJSON: ${superJsonTime}ms for ${iterations} iterations`);
console.log(`JSON: ${jsonTime}ms for ${iterations} iterations`);
console.log(`SuperJSON overhead: ${((superJsonTime - jsonTime) / jsonTime * 100).toFixed(1)}%`);
}
// テストデータ生成
const testData: BenchmarkData[] = Array.from({ length: 100 }, (_, i) => ({
id: i,
created: new Date(Date.now() - i * 1000),
values: Array.from({ length: 50 }, (_, j) => Math.random() * 100),
metadata: new Map([
['index', i],
['timestamp', Date.now()],
['random', Math.random()]
])
}));
benchmarkSerialization(testData, 100);
複雑なネストされたデータ
import superjson from 'superjson';
interface ComplexData {
id: number;
metadata: {
created: Date;
tags: Set<string>;
relations: Map<string, {
type: string;
target: number;
created: Date;
}>;
};
config: {
features: Set<string>;
settings: Map<string, any>;
thresholds: {
min: BigInt;
max: BigInt;
};
};
}
const complexData: ComplexData = {
id: 1,
metadata: {
created: new Date(),
tags: new Set(['important', 'user-data', 'analytics']),
relations: new Map([
['parent', { type: 'hierarchical', target: 0, created: new Date('2023-01-01') }],
['sibling', { type: 'peer', target: 2, created: new Date('2023-01-02') }]
])
},
config: {
features: new Set(['auth', 'logging', 'metrics']),
settings: new Map([
['theme', 'dark'],
['language', 'en'],
['notifications', true]
]),
thresholds: {
min: BigInt('1000000'),
max: BigInt('999999999999')
}
}
};
// 複雑なネストされたデータをシリアライズ
const serialized = superjson.stringify(complexData);
console.log('Serialized complex nested data');
// 複雑なネストされたデータをデシリアライズ
const deserialized = superjson.parse(serialized) as ComplexData;
console.log('ID:', deserialized.id);
console.log('Created:', deserialized.metadata.created);
console.log('Tags:', Array.from(deserialized.metadata.tags));
console.log('Features:', Array.from(deserialized.config.features));
console.log('Min threshold:', deserialized.config.thresholds.min.toString());
// 型チェック
console.log('All types preserved correctly:');
console.log('- Date:', deserialized.metadata.created instanceof Date);
console.log('- Set:', deserialized.metadata.tags instanceof Set);
console.log('- Map:', deserialized.metadata.relations instanceof Map);
console.log('- BigInt:', typeof deserialized.config.thresholds.min === 'bigint');
APIクライアントでの使用
import superjson from 'superjson';
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async post<T>(endpoint: string, data: any): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: superjson.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
return superjson.parse(text) as T;
}
async get<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
return superjson.parse(text) as T;
}
}
// 使用例
const apiClient = new ApiClient('https://api.example.com');
interface UserData {
id: number;
name: string;
createdAt: Date;
preferences: Map<string, any>;
}
async function updateUser(userData: UserData): Promise<UserData> {
try {
const updatedUser = await apiClient.post<UserData>('/users', userData);
console.log('User updated:', updatedUser.name);
console.log('Created at:', updatedUser.createdAt);
return updatedUser;
} catch (error) {
console.error('Failed to update user:', error);
throw error;
}
}