js-bson

シリアライゼーションTypeScriptBSONMongoDBバイナリフォーマット

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

js-bson

概要

js-bsonは、MongoDB公式のBSON(Binary JSON)パーサーです。Node.jsとブラウザ環境の両方で動作し、JSONライクなドキュメントを効率的なバイナリフォーマットで処理します。ObjectId、Long、Decimal128など、BSONの豊富なデータ型をサポートし、Extended JSON(EJSON)機能も提供しています。

詳細

BSONは「Binary JSON」の略で、JSONライクなドキュメントをバイナリエンコードするフォーマットです。MongoDBの内部データ形式として使用され、js-bsonはこのフォーマットの公式TypeScript/JavaScript実装を提供しています。

主な特徴:

  • クロスプラットフォーム: Node.jsとブラウザ環境の両方をサポート
  • 豊富なデータ型: ObjectId、Long、Decimal128、Binary、UUIDなど
  • Extended JSON: 人間が読める形式との相互変換
  • TypeScript対応: 完全な型定義とTypeScript支援
  • toBSON()フック: カスタムシリアライゼーションロジック

技術的詳細:

  • コア関数: serialize()、deserialize()、calculateObjectSize()
  • ストリーム対応: deserializeStream()によるストリーム処理
  • 設定オプション: checkKeys、serializeFunctions、ignoreUndefined
  • エラーハンドリング: BSONError.isBSONError()による型安全なエラーチェック

BSONデータ型:

  • Binary(バイナリデータ、UUID)
  • ObjectId(ユニークID)
  • Long(64bit整数)
  • Decimal128(高精度小数)
  • Double、Int32、Timestamp
  • Code(JavaScriptコード)
  • DBRef(参照)

メリット・デメリット

メリット

  • MongoDBエコシステムとの完全な互換性
  • JSONより効率的なバイナリ表現
  • 豊富なデータ型によるスキーマレス設計
  • TypeScript完全対応による型安全性
  • Node.jsとブラウザの両方で動作
  • Extended JSONによる人間が読める形式

デメリット

  • JSONと比べて人間が読みにくい
  • MongoDBに特化したフォーマット
  • ファイルサイズがやや大きい
  • デバッグ時のデータ確認が困難
  • 一般的なJSONツールとの互換性がない

参考ページ

書き方の例

基本的な使用方法

import { BSON } from 'bson';

interface User {
  name: string;
  age: number;
  email: string;
  createdAt: Date;
}

const user: User = {
  name: 'Alice',
  age: 30,
  email: '[email protected]',
  createdAt: new Date()
};

// BSONにシリアライズ
const serialized = BSON.serialize(user);
console.log('Serialized:', serialized.length, 'bytes');

// BSONからデシリアライズ
const deserialized = BSON.deserialize(serialized) as User;
console.log('Deserialized:', deserialized);

ObjectIdの使用

import { BSON, ObjectId } from 'bson';

interface Document {
  _id: ObjectId;
  title: string;
  content: string;
  tags: string[];
}

const doc: Document = {
  _id: new ObjectId(),
  title: 'My Document',
  content: 'This is the content of my document.',
  tags: ['typescript', 'mongodb', 'bson']
};

// BSONにシリアライズ
const serialized = BSON.serialize(doc);
console.log('Document ID:', doc._id.toHexString());
console.log('Serialized size:', serialized.length);

// BSONからデシリアライズ
const deserialized = BSON.deserialize(serialized) as Document;
console.log('Deserialized ID:', deserialized._id.toHexString());

Long型(64bit整数)の使用

import { BSON, Long } from 'bson';

interface Statistics {
  totalViews: Long;
  totalUsers: Long;
  revenue: number;
}

const stats: Statistics = {
  totalViews: Long.fromNumber(1234567890123),
  totalUsers: Long.fromNumber(987654321),
  revenue: 12345.67
};

// BSONにシリアライズ
const serialized = BSON.serialize(stats);

// BSONからデシリアライズ
const deserialized = BSON.deserialize(serialized) as Statistics;
console.log('Total views:', deserialized.totalViews.toString());
console.log('Total users:', deserialized.totalUsers.toNumber());

Decimal128の使用

import { BSON, Decimal128 } from 'bson';

interface FinancialData {
  productId: string;
  price: Decimal128;
  tax: Decimal128;
  total: Decimal128;
}

const data: FinancialData = {
  productId: 'PROD-001',
  price: Decimal128.fromString('99.99'),
  tax: Decimal128.fromString('8.00'),
  total: Decimal128.fromString('107.99')
};

// BSONにシリアライズ
const serialized = BSON.serialize(data);

// BSONからデシリアライズ
const deserialized = BSON.deserialize(serialized) as FinancialData;
console.log('Price:', deserialized.price.toString());
console.log('Tax:', deserialized.tax.toString());
console.log('Total:', deserialized.total.toString());

バイナリデータの処理

import { BSON, Binary } from 'bson';

interface FileDocument {
  filename: string;
  contentType: string;
  data: Binary;
  size: number;
}

// バイナリデータを作成
const imageData = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
const binary = new Binary(imageData);

const file: FileDocument = {
  filename: 'image.png',
  contentType: 'image/png',
  data: binary,
  size: imageData.length
};

// BSONにシリアライズ
const serialized = BSON.serialize(file);

// BSONからデシリアライズ
const deserialized = BSON.deserialize(serialized) as FileDocument;
console.log('Filename:', deserialized.filename);
console.log('Size:', deserialized.size);
console.log('Binary data length:', deserialized.data.buffer.length);

Extended JSON(EJSON)の使用

import { BSON, EJSON, ObjectId, Long } from 'bson';

interface ComplexDocument {
  _id: ObjectId;
  timestamp: Date;
  count: Long;
  metadata: {
    version: string;
    flags: boolean[];
  };
}

const doc: ComplexDocument = {
  _id: new ObjectId(),
  timestamp: new Date(),
  count: Long.fromNumber(1234567890),
  metadata: {
    version: '1.0.0',
    flags: [true, false, true]
  }
};

// Extended JSONに変換
const ejson = EJSON.stringify(doc, null, 2);
console.log('Extended JSON:');
console.log(ejson);

// Extended JSONからパース
const parsed = EJSON.parse(ejson) as ComplexDocument;
console.log('Parsed ID:', parsed._id.toHexString());
console.log('Parsed timestamp:', parsed.timestamp);
console.log('Parsed count:', parsed.count.toString());

カスタムシリアライゼーション(toBSON)

import { BSON, ObjectId } from 'bson';

class User {
  public _id: ObjectId;
  public name: string;
  public email: string;
  private password: string;

  constructor(name: string, email: string, password: string) {
    this._id = new ObjectId();
    this.name = name;
    this.email = email;
    this.password = password;
  }

  // カスタムシリアライゼーション
  toBSON() {
    return {
      _id: this._id,
      name: this.name,
      email: this.email,
      // パスワードは除外
      hasPassword: !!this.password
    };
  }
}

const user = new User('Alice', '[email protected]', 'secret123');

// BSONにシリアライズ(toBSONが自動的に呼ばれる)
const serialized = BSON.serialize(user);

// BSONからデシリアライズ
const deserialized = BSON.deserialize(serialized);
console.log('Deserialized:', deserialized);
// パスワードは含まれない

ストリーム処理

import { BSON } from 'bson';

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

const logEntries: LogEntry[] = [
  {
    timestamp: new Date(),
    level: 'INFO',
    message: 'Application started',
    metadata: { pid: 12345 }
  },
  {
    timestamp: new Date(),
    level: 'ERROR',
    message: 'Database connection failed',
    metadata: { error: 'ECONNREFUSED' }
  }
];

// 複数のドキュメントをシリアライズ
const serializedEntries = logEntries.map(entry => BSON.serialize(entry));
const totalSize = serializedEntries.reduce((sum, data) => sum + data.length, 0);

console.log(`Serialized ${logEntries.length} entries in ${totalSize} bytes`);

// ストリームからデシリアライズ
const deserializedEntries = serializedEntries.map(data => 
  BSON.deserialize(data) as LogEntry
);

deserializedEntries.forEach((entry, index) => {
  console.log(`Entry ${index}: ${entry.level} - ${entry.message}`);
});

エラーハンドリング

import { BSON, BSONError } from 'bson';

interface SafeDocument {
  name: string;
  value: number;
}

function safeBSONOperation(doc: SafeDocument): boolean {
  try {
    // BSONにシリアライズ
    const serialized = BSON.serialize(doc);
    
    // BSONからデシリアライズ
    const deserialized = BSON.deserialize(serialized) as SafeDocument;
    
    // バリデーション
    if (!deserialized.name || deserialized.value < 0) {
      throw new Error('Invalid document data');
    }
    
    console.log('Operation successful:', deserialized);
    return true;
    
  } catch (error) {
    if (BSONError.isBSONError(error)) {
      console.error('BSON error:', error.message);
    } else {
      console.error('General error:', error);
    }
    return false;
  }
}

// 正常なケース
safeBSONOperation({ name: 'Test', value: 42 });

// 異常なケース
safeBSONOperation({ name: '', value: -1 });

パフォーマンス測定

import { BSON } from 'bson';

interface BenchmarkData {
  id: number;
  values: number[];
  metadata: string;
}

function benchmarkBSON(data: BenchmarkData, iterations: number): void {
  const start = Date.now();
  
  for (let i = 0; i < iterations; i++) {
    // シリアライズ
    const serialized = BSON.serialize(data);
    
    // デシリアライズ
    const deserialized = BSON.deserialize(serialized);
  }
  
  const duration = Date.now() - start;
  console.log(`BSON: ${iterations} iterations in ${duration}ms`);
  console.log(`Average: ${duration / iterations}ms per iteration`);
}

const testData: BenchmarkData = {
  id: 12345,
  values: Array.from({ length: 1000 }, (_, i) => i * 0.1),
  metadata: 'benchmark_data'.repeat(10)
};

benchmarkBSON(testData, 1000);

設定オプション

import { BSON } from 'bson';

interface ConfigurableDocument {
  name: string;
  value: number;
  func?: Function;
  optional?: string;
}

const doc: ConfigurableDocument = {
  name: 'Test',
  value: 42,
  func: function() { return 'hello'; },
  optional: undefined
};

// カスタム設定でシリアライズ
const serialized = BSON.serialize(doc, {
  checkKeys: true,              // キーの検証
  serializeFunctions: true,     // 関数のシリアライズ
  ignoreUndefined: true         // undefinedの無視
});

// カスタム設定でデシリアライズ
const deserialized = BSON.deserialize(serialized, {
  promoteBuffers: true,         // Bufferの昇格
  promoteLongs: true,           // Longの昇格
  promoteValues: true           // 値の昇格
});

console.log('Deserialized:', deserialized);