SuperJSON
SuperJSON
SuperJSON
概要
SuperJSONは、Date、BigInt、RegExp、Map、Set、Errorなど、標準のJSONではサポートされていないJavaScriptの複雑な型を安全にシリアライズできるライブラリです。JSON.stringifyとJSON.parseの薄いラッパーとして機能し、メタデータを使用して型情報を保持します。特にNext.jsのgetServerSidePropsやgetStaticPropsなどのデータフェッチング関数で、Dateオブジェクトなどの非標準型を扱う際に便利です。
詳細
SuperJSONは、標準のJSONシリアライゼーションの制限を克服するために設計されたライブラリです。以下の特徴があります:
サポートされる型
標準JSON型:
- string、number、boolean、null、Array、Object
拡張型(デフォルトでサポート):
- undefined: オブジェクトから省略されず、配列でnullに変換されない
- BigInt: 文字列として保存され、型情報をメタデータで保持
- Date: ISO文字列として保存され、デシリアライズ時にDateオブジェクトに復元
- RegExp: パターン文字列として保存
- Set: 配列として保存され、メタデータでSet型を識別
- Map: キー・バリューペアの配列として保存
- Error: nameとmessageを含むオブジェクトとして保存
- URL: 文字列として保存され、メタデータでURL型を識別
- TypedArray: 配列として保存され、型情報をメタデータで保持
- 特殊な数値: NaN、Infinity、-Infinity、-0
動作原理
SuperJSONは、データをjson(JSONコンパチブルなデータ)とmeta(型情報などのメタデータ)の2つの部分に分けて保存します。これにより、標準のJSONパーサーでも基本的なデータは読み取れる一方、SuperJSONを使用すれば完全な型情報を復元できます。
TypeScript統合
TypeScriptで書かれているため、強力な型安全性を提供し、デシリアライズされた値の自動補完と型チェックが可能です。
メリット・デメリット
メリット
- 型の保持: Date、Map、Setなどの複雑なJavaScript型を正確に保存・復元
- 参照の同一性保持: 同じオブジェクトへの複数の参照を正しく復元
- TypeScript対応: 完全な型安全性と自動補完
- 軽量: 最小限のバンドルサイズを維持(デフォルトでは組み込み型のみサポート)
- Next.js統合: SWCプラグインやBabelプラグインで簡単に統合可能
- カスタム型のサポート: registerCustomでカスタム型の登録が可能
- 後方互換性: 標準JSONとして読み取り可能(メタデータなしでも基本データは取得可能)
- エラーハンドリング: Errorオブジェクトのシリアライゼーションをサポート
デメリット
- 追加の依存関係: プロジェクトに外部ライブラリを追加する必要がある
- パフォーマンス: 標準のJSON.stringify/parseより若干遅い
- ファイルサイズ: メタデータのため、出力サイズが標準JSONより大きくなる
- 学習コスト: 基本的な使用は簡単だが、高度な機能には理解が必要
- 互換性: SuperJSONを使用しないシステムとの相互運用時にメタデータの処理が必要
- メンテナンス: Next.js用プラグインのメンテナンス状況に不安がある(2025年時点)
参考ページ
- SuperJSON GitHub リポジトリ
- SuperJSON npm パッケージ
- next-superjson-plugin
- SuperJSON ドキュメント
- Blitz.js SuperJSON統合
書き方の例
Hello World - 基本的な使用方法
import superjson from 'superjson';
// シリアライゼーション
const data = {
message: "Hello World",
createdAt: new Date(2025, 0, 1),
pattern: /hello/gi,
tags: new Set(['greeting', 'example'])
};
const jsonString = superjson.stringify(data);
console.log(jsonString);
// 出力: {"json":{"message":"Hello World","createdAt":"2025-01-01T00:00:00.000Z","pattern":"/hello/gi","tags":["greeting","example"]},"meta":{"values":{"createdAt":["Date"],"pattern":["regexp"],"tags":["set"]}}}
// デシリアライゼーション
const parsed = superjson.parse(jsonString);
console.log(parsed.createdAt instanceof Date); // true
console.log(parsed.tags instanceof Set); // true
Next.jsとの統合
// pages/api/user.ts
import superjson from 'superjson';
export default function handler(req, res) {
const userData = {
id: 1,
name: "田中太郎",
registeredAt: new Date(),
tags: new Set(['admin', 'verified']),
metadata: new Map([
['lastLogin', new Date()],
['loginCount', BigInt(100)]
])
};
// SuperJSONでシリアライズしてレスポンス
res.status(200).json(superjson.serialize(userData));
}
// pages/index.tsx
import { GetServerSideProps } from 'next';
import superjson from 'superjson';
// next-superjson-pluginを使用している場合
export const getServerSideProps: GetServerSideProps = async () => {
const user = {
name: "山田花子",
createdAt: new Date(),
preferences: new Map([
['theme', 'dark'],
['language', 'ja']
])
};
return {
props: {
user // プラグインが自動的にSuperJSONでシリアライズ
}
};
};
// コンポーネントでは通常のオブジェクトとして受け取れる
export default function HomePage({ user }) {
console.log(user.createdAt instanceof Date); // true
console.log(user.preferences instanceof Map); // true
return (
<div>
<h1>{user.name}さん、ようこそ!</h1>
<p>登録日: {user.createdAt.toLocaleDateString('ja-JP')}</p>
</div>
);
}
カスタム型の登録
import superjson from 'superjson';
import { Decimal } from 'decimal.js';
// Decimal.js型のカスタムシリアライザーを登録
superjson.registerCustom<Decimal, string>(
{
isApplicable: (v): v is Decimal => Decimal.isDecimal(v),
serialize: (v) => v.toJSON(),
deserialize: (v) => new Decimal(v),
},
'decimal.js'
);
// カスタムクラスの例
class Point {
constructor(public x: number, public y: number) {}
distance() {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
}
// カスタムクラスの登録
superjson.registerClass(Point);
// 使用例
const data = {
price: new Decimal('99.99'),
location: new Point(3, 4)
};
const serialized = superjson.stringify(data);
const deserialized = superjson.parse<typeof data>(serialized);
console.log(deserialized.price.toString()); // "99.99"
console.log(deserialized.location.distance()); // 5
エラーハンドリングとセキュリティ
import superjson from 'superjson';
// エラーオブジェクトのシリアライゼーション
class CustomError extends Error {
constructor(message: string, public code: string) {
super(message);
this.name = 'CustomError';
}
}
const error = new CustomError('Something went wrong', 'ERR_001');
const serializedError = superjson.stringify({ error });
const { error: deserializedError } = superjson.parse(serializedError);
console.log(deserializedError instanceof Error); // true
console.log(deserializedError.message); // "Something went wrong"
// 安全なデシリアライゼーション
function safeParse<T>(jsonString: string): T | null {
try {
return superjson.parse<T>(jsonString);
} catch (error) {
console.error('Failed to parse SuperJSON:', error);
return null;
}
}
高度な使用例 - serializeとdeserialize
import superjson from 'superjson';
// serialize/deserializeを使用した詳細な制御
const complexData = {
user: {
id: 1,
name: "佐藤次郎",
birthday: new Date(1990, 5, 15),
permissions: new Set(['read', 'write', 'admin'])
},
session: {
token: 'abc123',
expiresAt: new Date(Date.now() + 3600000),
metadata: new Map([
['ip', '192.168.1.1'],
['userAgent', 'Mozilla/5.0...']
])
}
};
// serializeでjsonとmetaを分離
const { json, meta } = superjson.serialize(complexData);
console.log('JSON部分:', JSON.stringify(json, null, 2));
console.log('メタデータ:', JSON.stringify(meta, null, 2));
// 必要に応じてjsonとmetaを別々に保存・送信可能
// 例: jsonはAPIレスポンス、metaはヘッダーに含める等
// 復元
const restored = superjson.deserialize({ json, meta });
console.log(restored.user.birthday instanceof Date); // true
console.log(restored.session.metadata instanceof Map); // true
状態管理ライブラリとの統合
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import superjson from 'superjson';
// Zustandストアの例
interface StoreState {
user: {
name: string;
lastSeen: Date;
tags: Set<string>;
} | null;
preferences: Map<string, any>;
setUser: (user: StoreState['user']) => void;
updatePreference: (key: string, value: any) => void;
}
const useStore = create<StoreState>()(
persist(
(set) => ({
user: null,
preferences: new Map(),
setUser: (user) => set({ user }),
updatePreference: (key, value) =>
set((state) => {
const newPrefs = new Map(state.preferences);
newPrefs.set(key, value);
return { preferences: newPrefs };
}),
}),
{
name: 'app-storage',
storage: {
getItem: (name) => {
const str = localStorage.getItem(name);
if (!str) return null;
return superjson.parse(str);
},
setItem: (name, value) => {
localStorage.setItem(name, superjson.stringify(value));
},
removeItem: (name) => localStorage.removeItem(name),
},
}
)
);
// 使用例
const Component = () => {
const { user, setUser } = useStore();
const handleLogin = () => {
setUser({
name: "新規ユーザー",
lastSeen: new Date(),
tags: new Set(['new', 'unverified'])
});
};
return (
<div>
{user && (
<p>最終ログイン: {user.lastSeen.toLocaleString('ja-JP')}</p>
)}
</div>
);
};