JSON (Native)

シリアライゼーションTypeScriptJSONネイティブ標準ライブラリ

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

JSON (Native)

概要

JSON(JavaScript Object Notation)は、TypeScriptおよびJavaScriptの標準的なデータ交換フォーマットです。ネイティブなJSON.stringify()とJSON.parse()メソッドを使用して、オブジェクトとJSON文字列の間で変換を行います。軽量で人間が読みやすく、Web開発において最も広く使用されているシリアライゼーションフォーマットです。

詳細

JSONは「JavaScript Object Notation」の略で、JavaScriptのオブジェクトリテラル記法に基づいたテキストベースのデータ交換フォーマットです。TypeScriptでは、標準のJSON APIを使用してオブジェクトの永続化、API通信、設定ファイルの管理などを行います。

主な特徴:

  • ネイティブサポート: ブラウザとNode.jsで標準提供
  • 人間が読める: テキストベースで視覚的に理解しやすい
  • 軽量: 最小限のオーバーヘッドでデータ表現
  • 言語非依存: 多くのプログラミング言語でサポート
  • Web標準: REST API、設定ファイル、データ交換で広く使用

技術的詳細:

  • JSON.stringify(): オブジェクトをJSON文字列に変換
  • JSON.parse(): JSON文字列をオブジェクトに変換
  • replacer関数: シリアライゼーション時のカスタマイズ
  • reviver関数: デシリアライゼーション時のカスタマイズ
  • スペース引数: 整形された出力の生成

対応データ型:

  • プリミティブ型(string、number、boolean、null)
  • オブジェクト(Object)
  • 配列(Array)
  • 制限事項:undefined、function、symbol、Date(文字列として扱われる)

メリット・デメリット

メリット

  • 標準ライブラリのため追加の依存関係不要
  • 人間が読みやすく、デバッグが容易
  • 軽量でパフォーマンスが良い
  • 多くのプログラミング言語でサポート
  • Web API、設定ファイル、データ交換で広く使用
  • TypeScriptの型システムと良好に統合

デメリット

  • Date、undefined、function、BigIntなどの型をサポートしない
  • 循環参照のあるオブジェクトを扱えない
  • バイナリデータの直接サポートなし
  • 大きなデータセットではパフォーマンスが劣る場合がある
  • 精度の高い数値計算には適さない場合がある

参考ページ

書き方の例

基本的な使用方法

interface User {
  id: number;
  name: string;
  email: string;
  active: boolean;
}

const user: User = {
  id: 1,
  name: 'Alice',
  email: '[email protected]',
  active: true
};

// オブジェクトをJSON文字列に変換
const jsonString = JSON.stringify(user);
console.log('JSON:', jsonString);

// JSON文字列をオブジェクトに変換
const parsedUser: User = JSON.parse(jsonString);
console.log('Parsed:', parsedUser);

整形された出力

interface Config {
  server: {
    host: string;
    port: number;
    ssl: boolean;
  };
  database: {
    host: string;
    port: number;
    name: string;
  };
  features: string[];
}

const config: Config = {
  server: {
    host: 'localhost',
    port: 8080,
    ssl: false
  },
  database: {
    host: 'localhost',
    port: 5432,
    name: 'myapp'
  },
  features: ['auth', 'logging', 'metrics']
};

// 整形されたJSON出力
const prettyJson = JSON.stringify(config, null, 2);
console.log('Pretty JSON:\n', prettyJson);

replacer関数によるカスタマイズ

interface SensitiveData {
  username: string;
  password: string;
  apiKey: string;
  email: string;
}

const sensitiveData: SensitiveData = {
  username: 'alice',
  password: 'secret123',
  apiKey: 'sk-1234567890abcdef',
  email: '[email protected]'
};

// 機密データを除外するreplacer関数
const safeReplacer = (key: string, value: any): any => {
  if (key === 'password' || key === 'apiKey') {
    return '[REDACTED]';
  }
  return value;
};

const safeJson = JSON.stringify(sensitiveData, safeReplacer, 2);
console.log('Safe JSON:\n', safeJson);

reviver関数によるカスタマイズ

interface DateDocument {
  id: number;
  title: string;
  createdAt: Date;
  updatedAt: Date;
}

const doc: DateDocument = {
  id: 1,
  title: 'My Document',
  createdAt: new Date('2023-01-01'),
  updatedAt: new Date('2023-01-15')
};

// Date型をISO文字列でシリアライズ
const jsonString = JSON.stringify(doc);
console.log('Serialized:', jsonString);

// reviver関数でDate型を復元
const dateReviver = (key: string, value: any): any => {
  if (key.includes('At') && typeof value === 'string') {
    return new Date(value);
  }
  return value;
};

const parsedDoc: DateDocument = JSON.parse(jsonString, dateReviver);
console.log('Parsed with dates:', parsedDoc);
console.log('createdAt is Date?', parsedDoc.createdAt instanceof Date);

配列の処理

interface Product {
  id: number;
  name: string;
  price: number;
  tags: string[];
}

const products: Product[] = [
  {
    id: 1,
    name: 'Laptop',
    price: 999.99,
    tags: ['electronics', 'computers']
  },
  {
    id: 2,
    name: 'Mouse',
    price: 29.99,
    tags: ['electronics', 'accessories']
  }
];

// 配列をJSON文字列に変換
const jsonArray = JSON.stringify(products);
console.log('JSON Array:', jsonArray);

// JSON文字列を配列に変換
const parsedProducts: Product[] = JSON.parse(jsonArray);
console.log('Parsed Products:', parsedProducts.length, 'items');

// 特定の条件でフィルタリング
const electronicProducts = parsedProducts.filter(p => 
  p.tags.includes('electronics')
);
console.log('Electronic products:', electronicProducts.length);

エラーハンドリング

function safeJsonParse<T>(jsonString: string): T | null {
  try {
    return JSON.parse(jsonString) as T;
  } catch (error) {
    console.error('JSON Parse Error:', error);
    return null;
  }
}

function safeJsonStringify<T>(obj: T): string | null {
  try {
    return JSON.stringify(obj);
  } catch (error) {
    console.error('JSON Stringify Error:', error);
    return null;
  }
}

// 使用例
const invalidJson = '{"name": "Alice", "age": 30,}'; // 末尾のカンマが無効
const result = safeJsonParse<User>(invalidJson);

if (result) {
  console.log('Valid JSON:', result);
} else {
  console.log('Invalid JSON provided');
}

深い階層のオブジェクト

interface NestedData {
  level1: {
    level2: {
      level3: {
        value: string;
        metadata: {
          created: string;
          author: string;
        };
      };
    };
  };
}

const nestedData: NestedData = {
  level1: {
    level2: {
      level3: {
        value: 'deep value',
        metadata: {
          created: '2023-01-01',
          author: 'Alice'
        }
      }
    }
  }
};

// 深い階層もそのまま処理
const jsonString = JSON.stringify(nestedData, null, 2);
console.log('Nested JSON:\n', jsonString);

const parsed: NestedData = JSON.parse(jsonString);
console.log('Deep value:', parsed.level1.level2.level3.value);

JSONスキーマ検証(手動)

interface ApiResponse {
  success: boolean;
  data?: any;
  error?: string;
}

function validateApiResponse(json: string): ApiResponse | null {
  try {
    const parsed = JSON.parse(json);
    
    // 必須フィールドの検証
    if (typeof parsed.success !== 'boolean') {
      throw new Error('success field must be boolean');
    }
    
    // 条件付きフィールドの検証
    if (parsed.success && !parsed.data) {
      throw new Error('data field required when success is true');
    }
    
    if (!parsed.success && !parsed.error) {
      throw new Error('error field required when success is false');
    }
    
    return parsed as ApiResponse;
  } catch (error) {
    console.error('Validation error:', error);
    return null;
  }
}

// テスト
const validResponse = '{"success": true, "data": {"id": 1, "name": "Alice"}}';
const invalidResponse = '{"success": true}'; // dataが欠けている

console.log('Valid:', validateApiResponse(validResponse));
console.log('Invalid:', validateApiResponse(invalidResponse));

大きなデータセットの処理

interface LogEntry {
  timestamp: string;
  level: string;
  message: string;
  metadata?: Record<string, any>;
}

function processLargeDataset(entries: LogEntry[]): void {
  const start = Date.now();
  
  // 大きなデータセットをJSON文字列に変換
  const jsonString = JSON.stringify(entries);
  const serializeTime = Date.now() - start;
  
  console.log(`Serialized ${entries.length} entries in ${serializeTime}ms`);
  console.log(`JSON size: ${jsonString.length} characters`);
  
  // JSON文字列を配列に変換
  const parseStart = Date.now();
  const parsed: LogEntry[] = JSON.parse(jsonString);
  const parseTime = Date.now() - parseStart;
  
  console.log(`Parsed ${parsed.length} entries in ${parseTime}ms`);
}

// 大きなデータセットを生成
const largeDataset: LogEntry[] = Array.from({ length: 10000 }, (_, i) => ({
  timestamp: new Date(Date.now() - i * 1000).toISOString(),
  level: i % 4 === 0 ? 'ERROR' : 'INFO',
  message: `Log message ${i}`,
  metadata: { requestId: `req-${i}` }
}));

processLargeDataset(largeDataset);

TypeScriptの型との統合

// 型ガードを使用した安全なJSONパース
function isUser(obj: any): obj is User {
  return obj && 
         typeof obj.id === 'number' &&
         typeof obj.name === 'string' &&
         typeof obj.email === 'string' &&
         typeof obj.active === 'boolean';
}

function parseUserJson(jsonString: string): User | null {
  try {
    const parsed = JSON.parse(jsonString);
    
    if (isUser(parsed)) {
      return parsed;
    } else {
      console.error('Invalid user data structure');
      return null;
    }
  } catch (error) {
    console.error('JSON parsing failed:', error);
    return null;
  }
}

// 使用例
const userJson = '{"id": 1, "name": "Alice", "email": "[email protected]", "active": true}';
const user = parseUserJson(userJson);

if (user) {
  console.log(`User ${user.name} (${user.email}) is ${user.active ? 'active' : 'inactive'}`);
}

JSONファイルの読み書き(Node.js)

import { promises as fs } from 'fs';
import { join } from 'path';

interface AppConfig {
  version: string;
  environment: string;
  features: {
    [key: string]: boolean;
  };
}

async function saveConfig(config: AppConfig, filename: string): Promise<void> {
  try {
    const jsonString = JSON.stringify(config, null, 2);
    await fs.writeFile(filename, jsonString, 'utf8');
    console.log(`Config saved to ${filename}`);
  } catch (error) {
    console.error('Error saving config:', error);
  }
}

async function loadConfig(filename: string): Promise<AppConfig | null> {
  try {
    const jsonString = await fs.readFile(filename, 'utf8');
    return JSON.parse(jsonString) as AppConfig;
  } catch (error) {
    console.error('Error loading config:', error);
    return null;
  }
}

// 使用例
const config: AppConfig = {
  version: '1.0.0',
  environment: 'production',
  features: {
    auth: true,
    logging: true,
    metrics: false
  }
};

const configPath = join(__dirname, 'config.json');

// 設定を保存
await saveConfig(config, configPath);

// 設定を読み込み
const loadedConfig = await loadConfig(configPath);
if (loadedConfig) {
  console.log('Loaded config:', loadedConfig);
}