SuperJSON

シリアライゼーションTypeScriptJSON拡張型フルスタック

シリアライゼーションライブラリ

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以降が必要
  • デバッグ時の可読性が低下する場合がある

参考ページ

書き方の例

基本的な使用方法

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