SuperJSON

シリアライゼーションTypeScriptJavaScriptNext.jsJSON

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年時点)

参考ページ

書き方の例

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>
  );
};