Protocol Buffers (protobufjs)

シリアライゼーションバイナリJavaScriptTypeScriptgRPCスキーマ高性能

ライブラリ

Protocol Buffers (protobufjs)

概要

Protocol Buffers(protobufjs)は、Googleが開発した高性能バイナリシリアライゼーション形式のJavaScript実装です。スキーマベースの最適化により、JSONよりも高速で効率的なデータ交換を実現します。gRPCとの組み合わせで特に威力を発揮し、マイクロサービスアーキテクチャにおけるサービス間通信において、型安全性と前方・後方互換性を提供する次世代のシリアライゼーション技術として注目されています。

詳細

protobufjs 7.4.0は2025年現在も活発に開発されており、フル機能版、軽量版(.protoパーサーなし)、最小版(静的コードのみ)の3つのビルドバリアントを提供しています。スキーマファースト設計により、言語間でのAPIの一貫性と型安全性を保証し、フィールドの追加や削除時の前方・後方互換性を維持できます。Node.jsとブラウザ環境の両方で動作し、TypeScriptの完全サポートとzero-copyデシリアライゼーションによる極めて高いパフォーマンスを実現します。

主な特徴

  • 高性能: JSONより6-10倍高速な処理性能
  • バイナリ形式: コンパクトなデータサイズで帯域幅を節約
  • スキーマ進化: 前方・後方互換性を維持したAPI進化
  • 型安全性: コンパイル時の型チェックとランタイムバリデーション
  • 多言語対応: 40以上の言語での相互運用性
  • gRPC統合: gRPCサービスとの完全統合

メリット・デメリット

メリット

  • JSONと比較して6-10倍高速な処理性能と30-50%のサイズ削減
  • スキーマベースによる型安全性とAPI設計の強制
  • 前方・後方互換性により安全なAPIの進化が可能
  • マイクロサービス間通信での標準的な選択肢
  • gRPCとの完全統合による高性能RPC通信
  • 多言語での完全な相互運用性と一貫したAPI

デメリット

  • 学習コストが高く、.protoスキーマ定義の習得が必要
  • バイナリ形式のため人間による直接的な読み書きが困難
  • 動的なスキーマ変更が困難で、事前設計が重要
  • デバッグ時にバイナリデータの可視化が困難
  • 小規模なアプリケーションではオーバースペック
  • JSONに比べて開発ツールやエディタサポートが限定的

参考ページ

書き方の例

基本的なセットアップ

# protobuf.jsのインストール
npm install protobufjs --save

# CLI ツールのインストール(開発時に使用)
npm install protobufjs-cli --save-dev

# TypeScript型定義(通常は自動的に含まれる)
# npm install @types/protobufjs  # 最新版では不要

.protoスキーマ定義

// user.proto
syntax = "proto3";

package userservice;

// ユーザー情報メッセージ
message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  repeated string roles = 4;
  google.protobuf.Timestamp created_at = 5;
  bool is_active = 6;
}

// ユーザー作成リクエスト
message CreateUserRequest {
  string name = 1;
  string email = 2;
  repeated string roles = 3;
}

// ユーザーサービス定義
service UserService {
  rpc CreateUser(CreateUserRequest) returns (User);
  rpc GetUser(GetUserRequest) returns (User);
  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
}

message GetUserRequest {
  int32 id = 1;
}

message ListUsersRequest {
  int32 page = 1;
  int32 page_size = 2;
  string filter = 3;
}

message ListUsersResponse {
  repeated User users = 1;
  int32 total_count = 2;
  int32 page = 3;
  int32 page_size = 4;
}

基本的なメッセージ操作

// ES6モジュール(推奨)
import protobuf from 'protobufjs';

// .protoファイルの読み込み
protobuf.load("user.proto", function(err, root) {
    if (err) throw err;

    // メッセージタイプの取得
    const User = root.lookupType("userservice.User");

    // 1. メッセージ作成
    const userData = {
        id: 123,
        name: "田中太郎",
        email: "[email protected]",
        roles: ["user", "admin"],
        created_at: {
            seconds: Math.floor(Date.now() / 1000),
            nanos: 0
        },
        is_active: true
    };

    // 2. バリデーション(任意だが推奨)
    const errMsg = User.verify(userData);
    if (errMsg) throw Error(errMsg);

    // 3. メッセージインスタンス作成
    const message = User.create(userData);
    console.log("Created message:", message);

    // 4. バイナリエンコード
    const buffer = User.encode(message).finish();
    console.log("Encoded buffer size:", buffer.length, "bytes");

    // 5. バイナリデコード
    const decodedMessage = User.decode(buffer);
    console.log("Decoded message:", decodedMessage);

    // 6. プレーンオブジェクトに変換
    const plainObject = User.toObject(decodedMessage, {
        longs: String,
        enums: String,
        bytes: String,
        defaults: true,
        arrays: true,
        objects: true
    });
    console.log("Plain object:", plainObject);
});

Promise形式の非同期操作

// Promise版での使用
async function demonstrateProtobuf() {
    try {
        // .protoファイルの非同期読み込み
        const root = await protobuf.load("user.proto");
        const User = root.lookupType("userservice.User");

        // 複数ユーザーの一括処理
        const users = [
            { id: 1, name: "田中太郎", email: "[email protected]", roles: ["admin"], is_active: true },
            { id: 2, name: "佐藤花子", email: "[email protected]", roles: ["user"], is_active: true },
            { id: 3, name: "鈴木一郎", email: "[email protected]", roles: ["user", "moderator"], is_active: false }
        ];

        // 一括エンコード
        const encodedUsers = users.map(userData => {
            const message = User.create(userData);
            return User.encode(message).finish();
        });

        console.log("Encoded", encodedUsers.length, "users");

        // 一括デコード
        const decodedUsers = encodedUsers.map(buffer => {
            const message = User.decode(buffer);
            return User.toObject(message, { defaults: true });
        });

        console.log("Decoded users:", decodedUsers);

        // サイズ比較(JSONとの比較)
        const jsonSize = JSON.stringify(users).length;
        const protobufSize = encodedUsers.reduce((total, buffer) => total + buffer.length, 0);
        
        console.log(`Size comparison:
            JSON: ${jsonSize} bytes
            Protobuf: ${protobufSize} bytes
            Reduction: ${((jsonSize - protobufSize) / jsonSize * 100).toFixed(1)}%`);

    } catch (error) {
        console.error("Protobuf error:", error);
    }
}

demonstrateProtobuf();

TypeScript型定義生成

# 静的コード生成(本番環境用)
npx pbjs -t static-module -w commonjs -o user_pb.js user.proto

# TypeScript定義ファイル生成
npx pbts -o user_pb.d.ts user_pb.js

# JSON記述子生成(軽量版用)
npx pbjs -t json-module -w commonjs -o user.json user.proto
// TypeScript での使用例
import { User, CreateUserRequest } from './user_pb';

// 型安全なメッセージ作成
const createUser = (name: string, email: string, roles: string[]): User => {
    const user = new User();
    user.setId(Math.floor(Math.random() * 1000000));
    user.setName(name);
    user.setEmail(email);
    user.setRolesList(roles);
    user.setIsActive(true);
    
    const timestamp = new google.protobuf.Timestamp();
    timestamp.fromDate(new Date());
    user.setCreatedAt(timestamp);
    
    return user;
};

// 型安全なエンコード・デコード
const processUser = (userData: { name: string; email: string; roles: string[] }) => {
    // エンコード
    const user = createUser(userData.name, userData.email, userData.roles);
    const bytes = user.serializeBinary();
    
    // デコード
    const decodedUser = User.deserializeBinary(bytes);
    
    return {
        id: decodedUser.getId(),
        name: decodedUser.getName(),
        email: decodedUser.getEmail(),
        roles: decodedUser.getRolesList(),
        isActive: decodedUser.getIsActive(),
        createdAt: decodedUser.getCreatedAt()?.toDate()
    };
};

// 使用例
const result = processUser({
    name: "田中太郎",
    email: "[email protected]", 
    roles: ["admin", "user"]
});

console.log("Processed user:", result);

高度なスキーマ操作

// プログラムによるスキーマ定義(.protoファイルなし)
import protobuf from 'protobufjs';

const { Root, Type, Field, Service, Method } = protobuf;

// メッセージタイプの定義
const User = new Type("User")
    .add(new Field("id", 1, "int32"))
    .add(new Field("name", 2, "string"))
    .add(new Field("email", 3, "string"))
    .add(new Field("roles", 4, "string", "repeated"))
    .add(new Field("metadata", 5, "google.protobuf.Struct"))
    .add(new Field("is_active", 6, "bool"));

// ネストしたメッセージの定義
const Address = new Type("Address")
    .add(new Field("street", 1, "string"))
    .add(new Field("city", 2, "string"))
    .add(new Field("postal_code", 3, "string"))
    .add(new Field("country", 4, "string"));

const UserWithAddress = new Type("UserWithAddress")
    .add(new Field("user", 1, "User"))
    .add(new Field("address", 2, "Address"));

// サービスの定義
const UserService = new Service("UserService")
    .add(new Method("GetUser", "rpc", "GetUserRequest", "User"))
    .add(new Method("CreateUser", "rpc", "CreateUserRequest", "User"));

// ルートの作成
const root = new Root()
    .define("userservice")
    .add(User)
    .add(Address) 
    .add(UserWithAddress)
    .add(UserService);

// 動的スキーマの使用
const processUserWithAddress = (userData, addressData) => {
    const UserWithAddressType = root.lookupType("userservice.UserWithAddress");
    
    const message = UserWithAddressType.create({
        user: userData,
        address: addressData
    });
    
    const buffer = UserWithAddressType.encode(message).finish();
    const decoded = UserWithAddressType.decode(buffer);
    
    return UserWithAddressType.toObject(decoded, { defaults: true });
};

// 使用例
const result = processUserWithAddress(
    {
        id: 123,
        name: "田中太郎",
        email: "[email protected]",
        roles: ["user"],
        is_active: true
    },
    {
        street: "1-2-3 東京区",
        city: "東京",
        postal_code: "100-0001",
        country: "Japan"
    }
);

console.log("User with address:", result);

gRPCサービスの実装

// gRPCクライアント実装
import protobuf from 'protobufjs';
import grpc from '@grpc/grpc-js';

// protobufサービスをgRPCクライアントに統合
protobuf.load("user.proto", function(err, root) {
    if (err) throw err;

    const UserService = root.lookupService("userservice.UserService");
    
    // gRPC RPC実装関数
    const rpcImpl = function(method, requestData, callback) {
        // gRPCクライアントの作成
        const Client = grpc.makeGenericClientConstructor({});
        const client = new Client(
            'localhost:50051',
            grpc.credentials.createInsecure()
        );

        // unary RPCコール
        client.makeUnaryRequest(
            method.name,
            arg => arg,
            arg => arg,
            requestData,
            callback
        );
    };

    // サービスインスタンスの作成
    const service = UserService.create(rpcImpl);

    // RPCメソッドの呼び出し(Promise版)
    const callGetUser = async (userId) => {
        try {
            const response = await service.getUser({ id: userId });
            console.log('User retrieved:', response);
            return response;
        } catch (error) {
            console.error('gRPC error:', error);
            throw error;
        }
    };

    // RPCメソッドの呼び出し(コールバック版)
    const callCreateUser = (userData, callback) => {
        service.createUser(userData, (err, response) => {
            if (err) {
                console.error('Create user error:', err);
                callback(err);
                return;
            }
            console.log('User created:', response);
            callback(null, response);
        });
    };

    // 使用例
    callGetUser(123);
    
    callCreateUser({
        name: "新規ユーザー",
        email: "[email protected]",
        roles: ["user"]
    }, (err, user) => {
        if (!err) {
            console.log("Successfully created user:", user);
        }
    });
});

パフォーマンス最適化とベンチマーク

// パフォーマンステスト用のデータ生成
function generateTestData(count) {
    const users = [];
    for (let i = 0; i < count; i++) {
        users.push({
            id: i,
            name: `User ${i}`,
            email: `user${i}@example.com`,
            roles: i % 3 === 0 ? ["admin", "user"] : ["user"],
            metadata: {
                created_at: new Date().toISOString(),
                last_login: new Date(Date.now() - Math.random() * 86400000).toISOString(),
                preferences: {
                    theme: i % 2 === 0 ? "dark" : "light",
                    notifications: Math.random() > 0.5
                }
            },
            is_active: Math.random() > 0.1
        });
    }
    return users;
}

// パフォーマンス測定関数
function measurePerformance(label, fn) {
    const start = process.hrtime.bigint();
    const result = fn();
    const end = process.hrtime.bigint();
    const duration = Number(end - start) / 1000000; // ナノ秒からミリ秒に変換
    console.log(`${label}: ${duration.toFixed(2)}ms`);
    return result;
}

// protobuf vs JSON ベンチマーク
async function performanceComparison() {
    const root = await protobuf.load("user.proto");
    const User = root.lookupType("userservice.User");
    
    const testData = generateTestData(10000);
    console.log(`Testing with ${testData.length} users`);

    // JSON シリアライゼーション
    const jsonData = measurePerformance('JSON.stringify', () => {
        return testData.map(user => JSON.stringify(user));
    });

    const jsonSize = jsonData.reduce((total, str) => total + str.length, 0);

    // JSON デシリアライゼーション
    const jsonParsed = measurePerformance('JSON.parse', () => {
        return jsonData.map(str => JSON.parse(str));
    });

    // Protobuf シリアライゼーション
    const protobufData = measurePerformance('Protobuf encode', () => {
        return testData.map(userData => {
            const message = User.create(userData);
            return User.encode(message).finish();
        });
    });

    const protobufSize = protobufData.reduce((total, buffer) => total + buffer.length, 0);

    // Protobuf デシリアライゼーション
    const protobufParsed = measurePerformance('Protobuf decode', () => {
        return protobufData.map(buffer => {
            const message = User.decode(buffer);
            return User.toObject(message);
        });
    });

    // 結果の比較
    console.log('\n=== Performance Results ===');
    console.log(`JSON size: ${(jsonSize / 1024).toFixed(2)} KB`);
    console.log(`Protobuf size: ${(protobufSize / 1024).toFixed(2)} KB`);
    console.log(`Size reduction: ${((jsonSize - protobufSize) / jsonSize * 100).toFixed(1)}%`);
    
    // データ整合性の確認
    const sampleIndex = 0;
    const jsonObj = jsonParsed[sampleIndex];
    const protobufObj = protobufParsed[sampleIndex];
    
    console.log('\n=== Data Integrity Check ===');
    console.log('JSON sample:', JSON.stringify(jsonObj, null, 2).substring(0, 200) + '...');
    console.log('Protobuf sample:', JSON.stringify(protobufObj, null, 2).substring(0, 200) + '...');
    
    console.log('Data integrity check:', 
        jsonObj.id === protobufObj.id && 
        jsonObj.name === protobufObj.name && 
        jsonObj.email === protobufObj.email ? 'PASSED' : 'FAILED'
    );
}

performanceComparison().catch(console.error);

エラーハンドリングとデバッグ

// 包括的なエラーハンドリング
async function robustProtobufProcessing() {
    try {
        const root = await protobuf.load("user.proto");
        const User = root.lookupType("userservice.User");

        // 1. スキーマバリデーションエラーの処理
        const invalidUserData = {
            id: "invalid_id", // 数値でなければならない
            name: 123, // 文字列でなければならない
            email: null, // 必須フィールド
            roles: "single_role", // 配列でなければならない
        };

        console.log("Testing schema validation...");
        const errMsg = User.verify(invalidUserData);
        if (errMsg) {
            console.error("Validation error:", errMsg);
            
            // エラーの詳細分析
            if (errMsg.includes("id")) {
                console.log("ID field must be a number");
            }
            if (errMsg.includes("name")) {
                console.log("Name field must be a string");
            }
        }

        // 2. 正しいデータでの処理
        const validUserData = {
            id: 123,
            name: "田中太郎",
            email: "[email protected]",
            roles: ["user", "admin"],
            is_active: true
        };

        const message = User.create(validUserData);
        const buffer = User.encode(message).finish();

        // 3. 破損したバッファのハンドリング
        console.log("Testing corrupted buffer handling...");
        const corruptedBuffer = Buffer.from([0x08, 0x96, 0x01, 0xFF, 0xFF]); // 不正なバイナリ

        try {
            const corruptedMessage = User.decode(corruptedBuffer);
            console.log("Unexpectedly decoded corrupted buffer:", corruptedMessage);
        } catch (decodeError) {
            console.log("Properly caught decode error:", decodeError.message);
        }

        // 4. フィールド欠損のハンドリング
        console.log("Testing missing required fields...");
        try {
            const incompleteBuffer = User.encode(User.create({ id: 456 })).finish();
            const incompleteMessage = User.decode(incompleteBuffer);
            
            // デフォルト値を適切に設定
            const completeObject = User.toObject(incompleteMessage, {
                defaults: true, // デフォルト値を含める
                arrays: true,   // 空配列を含める
                objects: true   // 空オブジェクトを含める
            });
            
            console.log("Incomplete message with defaults:", completeObject);
        } catch (error) {
            console.error("Error processing incomplete message:", error);
        }

        // 5. 型変換エラーのハンドリング
        console.log("Testing type conversion...");
        const mixedTypeData = {
            id: 123,
            name: "田中太郎",
            email: "[email protected]",
            roles: ["user"],
            created_at: new Date(), // Dateオブジェクト - Timestamp変換が必要
            is_active: "true" // 文字列 - boolean変換が必要
        };

        // カスタムオブジェクト変換
        const sanitizeUserData = (data) => {
            const sanitized = { ...data };
            
            // boolean変換
            if (typeof sanitized.is_active === 'string') {
                sanitized.is_active = sanitized.is_active.toLowerCase() === 'true';
            }
            
            // Date変換
            if (sanitized.created_at instanceof Date) {
                sanitized.created_at = {
                    seconds: Math.floor(sanitized.created_at.getTime() / 1000),
                    nanos: (sanitized.created_at.getTime() % 1000) * 1000000
                };
            }
            
            return sanitized;
        };

        const sanitizedData = sanitizeUserData(mixedTypeData);
        const sanitizedMessage = User.create(sanitizedData);
        const sanitizedBuffer = User.encode(sanitizedMessage).finish();
        const decodedSanitized = User.decode(sanitizedBuffer);
        
        console.log("Sanitized and processed data:", User.toObject(decodedSanitized));

    } catch (error) {
        console.error("Unexpected error in protobuf processing:", error);
        
        // エラーのタイプ別処理
        if (error.message.includes("Protocol error")) {
            console.log("This is a protocol-level error");
        } else if (error.message.includes("JSON")) {
            console.log("This is a JSON-related error");
        } else {
            console.log("This is a general error");
        }
    }
}

robustProtobufProcessing();