T-CREATOR

Gemini CLI で JSON が壊れる問題を撲滅:出力拘束・スキーマ・再試行の実務

Gemini CLI で JSON が壊れる問題を撲滅:出力拘束・スキーマ・再試行の実務

AI を活用した開発において、Gemini CLI から取得する JSON データの信頼性は業務の成否を左右する重要な要素です。しかし、現実には「期待した JSON 形式で出力されない」「途中でデータが切れてしまう」「スキーマに合わない内容が混入する」といった問題に直面することが少なくありません。

こうした JSON 出力の不安定性は、単なる技術的な課題を超えて、システム全体の信頼性やユーザー体験に深刻な影響を与える可能性があります。本記事では、Gemini CLI における JSON 出力の問題を根本から解決するための実践的なアプローチをご紹介します。

出力拘束によるデータ形式の統制、JSON スキーマを用いた厳密な検証、そして堅牢な再試行メカニズムの実装という 3 つの軸から、信頼性の高い JSON 出力システムを構築する方法を詳しく解説いたします。これらの手法を組み合わせることで、JSON 破損問題を効果的に撲滅し、安定したサービス運用を実現できるでしょう。

背景

Gemini CLI の JSON 出力の仕組み

Gemini CLI は、Google の生成 AI モデルを活用してテキスト生成を行うコマンドラインツールです。JSON 形式での出力を指定する際、内部的には以下の処理フローで動作しています。

mermaidflowchart LR
  user["開発者"] -->|プロンプト送信| cli["Gemini CLI"]
  cli -->|API リクエスト| gemini["Gemini API"]
  gemini -->|テキスト生成| response["生成結果"]
  response -->|JSON パース| cli
  cli -->|標準出力| user

このフローにおいて、Gemini API からの応答は基本的にテキスト形式であり、JSON として解釈できるかどうかは生成されたテキストの品質に依存します。プロンプトで JSON 形式を指定しても、モデルが必ずしも完璧な JSON を生成するとは限らないのが現実です。

通常の使用では、以下のようなコマンドで JSON 出力を期待します。

typescript// Gemini CLI での JSON 出力指定例
const command = `gemini generate --prompt "以下の情報をJSONで出力してください: ${inputData}" --format json`;

しかし、この単純な指定だけでは、出力の一貫性や形式の正確性を保証することができません。

よくある JSON 破損パターン

実際の運用において遭遇する典型的な JSON 破損パターンを分類してみましょう。これらのパターンを理解することで、適切な対策を講じることができます。

#破損パターン具体例発生頻度
1不完全な出力{"name": "example", "value": 1
2余計な文字の混入Here is the JSON: {"name": "example"}
3エスケープエラー{"message": "This is "quoted" text"}
4型の不整合{"count": "five", "active": "true"}
5未定義フィールド{"name": "example", "unknown_field": null}

1. 不完全な出力(最も頻繁)

最も頻繁に発生するのが、JSON の途中で出力が終了してしまうケースです。

json{
  "users": [
    {"id": 1, "name": "Alice"},
    {"id": 2, "name": "Bob"},
    {"id": 3, "name":

この問題は、API の応答文字数制限や、生成プロセスの途中での処理終了が原因となることが多いです。

2. 余計な文字の混入

生成された JSON の前後に説明文や装飾文字が含まれるケースも頻繁に見られます。

text以下が要求された JSON データです:

{"result": "success", "data": {"count": 42}}

以上が生成結果となります。

このような出力では、純粋な JSON 部分だけを抽出する処理が必要になります。

問題が発生する原因分析

JSON 破損が発生する根本的な原因を技術的な観点から分析してみましょう。

mermaidflowchart TD
  root["JSON破損の発生"] --> model["モデル起因"]
  root --> api["API起因"]
  root --> client["クライアント起因"]

  model --> model1["プロンプト理解の曖昧性"]
  model --> model2["文脈窓の制限"]
  model --> model3["生成の確率的性質"]

  api --> api1["レスポンス文字数制限"]
  api --> api2["ネットワーク切断"]
  api --> api3["レート制限"]

  client --> client1["パースエラー処理不備"]
  client --> client2["エラーハンドリング不足"]
  client --> client3["検証ロジックの欠如"]

モデル起因の問題

生成 AI モデルの特性上、同じプロンプトでも毎回異なる出力が生成される確率的な性質があります。JSON のような厳密な構造を持つデータフォーマットであっても、完璧な形式で出力される保証はありません。

特に長大な JSON を生成する場合、モデルの文脈窓(コンテキストウィンドウ)の制限により、出力の途中で構造の整合性を保てなくなることがあります。

API 起因の問題

Gemini API 自体の制限も破損の原因となります。単一のレスポンスで返却可能な文字数に上限があり、大きなデータセットを JSON で出力しようとした際に途中で切れてしまうケースが発生します。

また、ネットワークの不安定性や API 側のレート制限により、リクエストが正常に完了しない場合もあります。

クライアント起因の問題

開発者側の実装においても、適切なエラーハンドリングや検証ロジックが不足していることが問題を深刻化させます。生成された文字列をそのまま JSON.parse() に渡すだけでは、破損したデータに対して適切に対処できません。

課題

出力形式の不安定性

Gemini CLI を用いた JSON 出力における最大の課題は、同一のプロンプトでも出力形式が一定しないことです。この不安定性は、以下のような具体的な問題を引き起こします。

mermaidstateDiagram-v2
    [*] --> PromptSent: プロンプト送信
    PromptSent --> ValidJSON: 正常なJSON生成
    PromptSent --> InvalidJSON: 異常なJSON生成
    PromptSent --> IncompleteJSON: 不完全なJSON生成

    ValidJSON --> [*]: 処理成功
    InvalidJSON --> RetryProcess: 再試行処理
    IncompleteJSON --> RetryProcess: 再試行処理

    RetryProcess --> PromptSent: リトライ
    RetryProcess --> [*]: 失敗終了

システム連携時の障害

マイクロサービス間の通信や外部システムとの連携において、JSON 形式の不整合は致命的な問題となります。特に、以下のようなケースで深刻な影響が生じます。

typescript// 期待される JSON 形式
interface ExpectedResponse {
  status: string;
  data: {
    items: Array<{ id: number; name: string }>;
    total: number;
  };
}

// 実際に生成される問題のある出力例
const problematicOutput = `{
  "status": "success",
  "data": {
    "items": [
      {"id": 1, "name": "Item 1"},
      {"id": 2, "name": "Item 2"
    // ここで出力が途切れる
`;

このような不完全な JSON は、後続の処理でパースエラーを引き起こし、システム全体の停止につながる可能性があります。

バッチ処理での影響拡大

大量のデータを処理するバッチジョブにおいて、JSON 出力の不安定性は影響範囲を大きく拡大させます。1 つの不正な JSON 出力が、バッチ全体の失敗を引き起こすケースも少なくありません。

スキーマ違反による破損

JSON スキーマで定義された構造に合わない出力が生成されることも、重要な課題の一つです。この問題は、単純なパースエラーよりも発見が困難で、データの不整合や予期しない動作を引き起こします。

データ型の不整合

数値として期待しているフィールドに文字列が入る、ブール値として期待している箇所に文字列の "true"/"false" が入るなど、データ型レベルでの不整合が頻繁に発生します。

typescript// 期待するスキーマ
interface ProductSchema {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
}

// 問題のある実際の出力
const problematicData = {
  id: '12345', // 文字列になっている
  name: 'Product A',
  price: '29.99', // 数値が文字列
  inStock: 'true', // ブール値が文字列
};

必須フィールドの欠如

スキーマで必須と定義されたフィールドが出力に含まれない、または null 値が設定されるケースも多く見られます。これらの問題は、アプリケーションの実行時エラーや予期しない動作の原因となります。

リトライ処理の不備

JSON 出力に問題が発生した際の再試行メカニズムの不備も、運用上の大きな課題となっています。単純な再試行では解決できない問題も多く、より洗練されたアプローチが必要です。

非効率的な再試行戦略

多くの実装では、固定間隔での単純な再試行しか行われていません。これでは API の負荷を無駄に増加させるだけでなく、根本的な解決にもつながりません。

typescript// 問題のある再試行実装例
async function generateWithSimpleRetry(
  prompt: string,
  maxRetries: number = 3
): Promise<object> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const result = await geminiGenerate(prompt);
      return JSON.parse(result);
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      // 問題:固定間隔での再試行、エラー分析なし
      await sleep(1000);
    }
  }
}

エラー分類の欠如

発生したエラーの種類を分析せずに一律に再試行を行うことで、解決不可能な問題に対して無駄な処理を繰り返してしまうケースがあります。パースエラー、ネットワークエラー、スキーマ違反など、エラーの種類に応じた適切な対応が必要です。

解決策

出力拘束(Output Constraints)の活用

JSON 出力の信頼性を向上させるための第一歩は、プロンプトレベルでの厳密な出力拘束の実装です。単に「JSON で出力してください」と指示するだけでなく、具体的な制約条件を明示することで、期待する形式での出力確率を大幅に向上させることができます。

mermaidflowchart TD
  prompt["基本プロンプト"] --> constraint1["形式指定"]
  prompt --> constraint2["例示提供"]
  prompt --> constraint3["制約条件"]

  constraint1 --> output1["JSON形式固定"]
  constraint2 --> output2["構造パターン統一"]
  constraint3 --> output3["バリデーション強化"]

  output1 --> result["安定した出力"]
  output2 --> result
  output3 --> result

段階的な拘束条件の実装

効果的な出力拘束は、以下の 3 段階のアプローチで実装します。

1. 基本的な形式指定

最も基本となるのは、出力形式を明確に指定することです。

typescriptconst basicConstraint = `
以下の条件を厳守してJSONを出力してください:
1. 純粋なJSONのみを出力し、説明文は一切含めない
2. 最外層は必ずオブジェクト({})で開始し、完全に閉じる
3. すべての文字列は二重引用符で囲む
4. 末尾のカンマは使用しない

出力例:
{"status": "success", "data": {"count": 10}}
`;

2. 構造の明示的定義

期待する JSON の構造を具体的に示すことで、出力の一貫性を確保します。

typescriptfunction createStructuredPrompt(
  dataSchema: object
): string {
  return `
以下のスキーマに厳密に従ってJSONを生成してください:

期待する構造:
${JSON.stringify(dataSchema, null, 2)}

制約条件:
- 指定された型と完全に一致する値のみ使用
- 必須フィールドはすべて含める
- 未定義のフィールドは追加しない
- null値は明示的に指定された場合のみ使用
`;
}

3. 検証可能な制約の追加

出力の正当性を機械的に検証できる条件を含めることで、品質の向上を図ります。

typescriptconst validationConstraints = `
追加制約:
1. 数値フィールドは必ず数値型で出力(文字列不可)
2. ブール値は true/false のみ(文字列不可)
3. 配列の要素数は0以上100以下
4. 文字列の長さは1000文字以内
5. オブジェクトのネストは5階層以内
`;

実用的なプロンプトテンプレート

実際のプロジェクトで活用できる、再利用可能なプロンプトテンプレートを構築します。

typescriptclass ConstraintedPromptBuilder {
  private constraints: string[] = [];
  private schema: object | null = null;
  private examples: object[] = [];

  setSchema(schema: object): this {
    this.schema = schema;
    return this;
  }

  addConstraint(constraint: string): this {
    this.constraints.push(constraint);
    return this;
  }

  addExample(example: object): this {
    this.examples.push(example);
    return this;
  }

  build(userPrompt: string): string {
    let prompt = userPrompt + '\n\n';

    prompt += '【出力形式の制約】\n';
    prompt += '1. 純粋なJSONのみ出力(説明文禁止)\n';
    prompt += '2. 完全な形式で出力(途中終了禁止)\n';

    if (this.constraints.length > 0) {
      prompt += '3. 追加制約:\n';
      this.constraints.forEach((constraint, index) => {
        prompt += `   ${index + 1}. ${constraint}\n`;
      });
    }

    if (this.schema) {
      prompt += '\n【期待するスキーマ】\n';
      prompt += JSON.stringify(this.schema, null, 2) + '\n';
    }

    if (this.examples.length > 0) {
      prompt += '\n【出力例】\n';
      this.examples.forEach((example, index) => {
        prompt += `例${index + 1}${JSON.stringify(
          example
        )}\n`;
      });
    }

    return prompt;
  }
}

JSON スキーマ検証の実装

出力拘束と並んで重要なのが、生成された JSON の厳密な検証です。JSON Schema を活用した包括的な検証システムを構築することで、スキーマ違反による問題を確実に防ぐことができます。

検証ライブラリの選択と設定

TypeScript/JavaScript 環境では、ajv ライブラリが最も実用的な選択肢です。高速で機能豊富な検証が可能です。

typescriptimport Ajv, { JSONSchemaType } from 'ajv';
import addFormats from 'ajv-formats';

class JSONValidator {
  private ajv: Ajv;

  constructor() {
    this.ajv = new Ajv({
      allErrors: true, // すべてのエラーを収集
      removeAdditional: true, // 未定義フィールドを除去
      coerceTypes: false, // 型強制を無効化(厳密な検証)
    });
    addFormats(this.ajv); // 日付、メール等の形式サポート
  }

  createValidator<T>(schema: JSONSchemaType<T>) {
    const validate = this.ajv.compile(schema);

    return (
      data: unknown
    ): { valid: boolean; data?: T; errors?: string[] } => {
      if (validate(data)) {
        return { valid: true, data: data as T };
      } else {
        const errors =
          validate.errors?.map(
            (err) => `${err.instancePath}: ${err.message}`
          ) || [];
        return { valid: false, errors };
      }
    };
  }
}

包括的なスキーマ定義

実用的なアプリケーションで必要となる、詳細なスキーマ定義の実装例です。

typescriptinterface UserProfile {
  id: number;
  name: string;
  email: string;
  age: number;
  isActive: boolean;
  tags: string[];
  metadata: Record<string, unknown>;
}

const userProfileSchema: JSONSchemaType<UserProfile> = {
  type: 'object',
  properties: {
    id: {
      type: 'integer',
      minimum: 1,
      maximum: 999999999,
    },
    name: {
      type: 'string',
      minLength: 1,
      maxLength: 100,
      pattern: '^[\\p{L}\\p{N}\\s\\-_.]+$', // Unicode文字、数字、空白、記号
    },
    email: {
      type: 'string',
      format: 'email',
      maxLength: 254,
    },
    age: {
      type: 'integer',
      minimum: 0,
      maximum: 150,
    },
    isActive: { type: 'boolean' },
    tags: {
      type: 'array',
      items: {
        type: 'string',
        minLength: 1,
        maxLength: 50,
      },
      maxItems: 20,
    },
    metadata: {
      type: 'object',
      additionalProperties: true, // 動的フィールドを許可
    },
  },
  required: [
    'id',
    'name',
    'email',
    'age',
    'isActive',
    'tags',
    'metadata',
  ],
  additionalProperties: false,
};

エラーハンドリングと修復処理

検証エラーが発生した際の、自動的な修復処理を実装します。

typescriptclass JSONRepairService {
  private validator: JSONValidator;

  constructor() {
    this.validator = new JSONValidator();
  }

  async validateAndRepair<T>(
    rawData: string,
    schema: JSONSchemaType<T>
  ): Promise<{
    success: boolean;
    data?: T;
    errors: string[];
  }> {
    const errors: string[] = [];

    // 1. 基本的なJSONパースを試行
    let parsedData: unknown;
    try {
      parsedData = JSON.parse(rawData);
    } catch (parseError) {
      // JSON修復を試行
      const repairedJSON = this.attemptJSONRepair(rawData);
      if (repairedJSON) {
        try {
          parsedData = JSON.parse(repairedJSON);
          errors.push('JSON構文エラーを自動修復しました');
        } catch {
          return {
            success: false,
            errors: ['JSON構文エラー:修復不可能'],
          };
        }
      } else {
        return {
          success: false,
          errors: ['JSON構文エラー:修復不可能'],
        };
      }
    }

    // 2. スキーマ検証
    const validate = this.validator.createValidator(schema);
    const result = validate(parsedData);

    if (result.valid) {
      return {
        success: true,
        data: result.data!,
        errors,
      };
    } else {
      errors.push(...(result.errors || []));

      // 3. データ修復を試行
      const repairedData = this.attemptDataRepair(
        parsedData,
        schema
      );
      if (repairedData) {
        const retryResult = validate(repairedData);
        if (retryResult.valid) {
          errors.push('スキーマ違反を自動修復しました');
          return {
            success: true,
            data: retryResult.data!,
            errors,
          };
        }
      }

      return { success: false, errors };
    }
  }

  private attemptJSONRepair(
    jsonString: string
  ): string | null {
    // 一般的なJSON修復パターン
    const repairs = [
      // 末尾の不完全な構造を補完
      (s: string) =>
        s +
        (s.split('{').length > s.split('}').length
          ? '}'
          : ''),
      // 末尾カンマの除去
      (s: string) => s.replace(/,(\s*[}\]])/g, '$1'),
      // 引用符の修復
      (s: string) =>
        s.replace(/([{,]\s*)(\w+)(\s*:)/g, '$1"$2"$3'),
    ];

    for (const repair of repairs) {
      try {
        const repaired = repair(jsonString);
        JSON.parse(repaired); // パース可能かテスト
        return repaired;
      } catch {
        continue;
      }
    }

    return null;
  }

  private attemptDataRepair<T>(
    data: unknown,
    schema: JSONSchemaType<T>
  ): unknown | null {
    if (typeof data !== 'object' || data === null)
      return null;

    const obj = data as Record<string, unknown>;
    const repaired = { ...obj };

    // 型強制による修復
    Object.entries(schema.properties || {}).forEach(
      ([key, propSchema]) => {
        if (key in repaired) {
          const value = repaired[key];

          // 文字列→数値変換
          if (
            propSchema.type === 'integer' ||
            propSchema.type === 'number'
          ) {
            if (
              typeof value === 'string' &&
              !isNaN(Number(value))
            ) {
              repaired[key] = Number(value);
            }
          }

          // 文字列→ブール変換
          if (propSchema.type === 'boolean') {
            if (value === 'true') repaired[key] = true;
            if (value === 'false') repaired[key] = false;
          }
        }
      }
    );

    return repaired;
  }
}

自動再試行メカニズムの構築

JSON 出力の信頼性を最大化するためには、単純な再試行ではなく、エラーの種類に応じた知的な再試行戦略が必要です。指数バックオフ、サーキットブレーカー、エラー分類による適応的制御を組み合わせたシステムを構築します。

指数バックオフによる再試行制御

API 負荷を適切に制御しながら、効果的な再試行を実現する仕組みです。

typescriptinterface RetryConfig {
  maxRetries: number;
  baseDelay: number;
  maxDelay: number;
  backoffFactor: number;
  jitter: boolean;
}

class ExponentialBackoffRetry {
  private config: RetryConfig;

  constructor(config: Partial<RetryConfig> = {}) {
    this.config = {
      maxRetries: 3,
      baseDelay: 1000,
      maxDelay: 30000,
      backoffFactor: 2,
      jitter: true,
      ...config,
    };
  }

  async execute<T>(
    operation: () => Promise<T>,
    shouldRetry: (
      error: unknown,
      attempt: number
    ) => boolean = () => true
  ): Promise<T> {
    let lastError: unknown;

    for (
      let attempt = 0;
      attempt <= this.config.maxRetries;
      attempt++
    ) {
      try {
        return await operation();
      } catch (error) {
        lastError = error;

        if (
          attempt === this.config.maxRetries ||
          !shouldRetry(error, attempt)
        ) {
          break;
        }

        const delay = this.calculateDelay(attempt);
        await this.sleep(delay);
      }
    }

    throw lastError;
  }

  private calculateDelay(attempt: number): number {
    const exponentialDelay = Math.min(
      this.config.baseDelay *
        Math.pow(this.config.backoffFactor, attempt),
      this.config.maxDelay
    );

    if (this.config.jitter) {
      // ジッターを追加して同時リクエストを分散
      return exponentialDelay * (0.5 + Math.random() * 0.5);
    }

    return exponentialDelay;
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) =>
      setTimeout(resolve, ms)
    );
  }
}

エラー分類による適応的制御

発生したエラーの種類に応じて、再試行戦略を動的に調整するシステムです。

typescriptenum ErrorType {
  NETWORK_ERROR = 'network',
  PARSE_ERROR = 'parse',
  SCHEMA_VALIDATION_ERROR = 'schema',
  RATE_LIMIT_ERROR = 'rate_limit',
  AUTHENTICATION_ERROR = 'auth',
  UNKNOWN_ERROR = 'unknown',
}

interface ErrorClassification {
  type: ErrorType;
  retryable: boolean;
  baseDelay: number;
  maxRetries: number;
}

class SmartRetryManager {
  private retryService: ExponentialBackoffRetry;
  private errorClassifier: ErrorClassifier;

  constructor() {
    this.retryService = new ExponentialBackoffRetry();
    this.errorClassifier = new ErrorClassifier();
  }

  async executeWithSmartRetry<T>(
    operation: () => Promise<T>
  ): Promise<{
    result: T;
    attempts: number;
    errors: string[];
  }> {
    const errors: string[] = [];
    let attempts = 0;

    const smartOperation = async (): Promise<T> => {
      attempts++;
      try {
        return await operation();
      } catch (error) {
        const classification =
          this.errorClassifier.classify(error);
        errors.push(
          `Attempt ${attempts}: ${classification.type} - ${error}`
        );

        if (!classification.retryable) {
          throw error;
        }

        // エラー種別に応じた遅延調整
        this.retryService = new ExponentialBackoffRetry({
          maxRetries: classification.maxRetries,
          baseDelay: classification.baseDelay,
        });

        throw error;
      }
    };

    const result = await this.retryService.execute(
      smartOperation,
      (error, attempt) => {
        const classification =
          this.errorClassifier.classify(error);
        return (
          classification.retryable &&
          attempt < classification.maxRetries
        );
      }
    );

    return { result, attempts, errors };
  }
}

class ErrorClassifier {
  private classifications: Map<
    ErrorType,
    ErrorClassification
  >;

  constructor() {
    this.classifications = new Map([
      [
        ErrorType.NETWORK_ERROR,
        {
          type: ErrorType.NETWORK_ERROR,
          retryable: true,
          baseDelay: 2000,
          maxRetries: 5,
        },
      ],
      [
        ErrorType.PARSE_ERROR,
        {
          type: ErrorType.PARSE_ERROR,
          retryable: true,
          baseDelay: 1000,
          maxRetries: 3,
        },
      ],
      [
        ErrorType.SCHEMA_VALIDATION_ERROR,
        {
          type: ErrorType.SCHEMA_VALIDATION_ERROR,
          retryable: true,
          baseDelay: 500,
          maxRetries: 2,
        },
      ],
      [
        ErrorType.RATE_LIMIT_ERROR,
        {
          type: ErrorType.RATE_LIMIT_ERROR,
          retryable: true,
          baseDelay: 5000,
          maxRetries: 10,
        },
      ],
      [
        ErrorType.AUTHENTICATION_ERROR,
        {
          type: ErrorType.AUTHENTICATION_ERROR,
          retryable: false,
          baseDelay: 0,
          maxRetries: 0,
        },
      ],
    ]);
  }

  classify(error: unknown): ErrorClassification {
    if (error instanceof Error) {
      const message = error.message.toLowerCase();

      if (
        message.includes('network') ||
        message.includes('timeout')
      ) {
        return this.classifications.get(
          ErrorType.NETWORK_ERROR
        )!;
      }

      if (
        message.includes('json') ||
        message.includes('parse')
      ) {
        return this.classifications.get(
          ErrorType.PARSE_ERROR
        )!;
      }

      if (
        message.includes('schema') ||
        message.includes('validation')
      ) {
        return this.classifications.get(
          ErrorType.SCHEMA_VALIDATION_ERROR
        )!;
      }

      if (
        message.includes('rate limit') ||
        message.includes('too many requests')
      ) {
        return this.classifications.get(
          ErrorType.RATE_LIMIT_ERROR
        )!;
      }

      if (
        message.includes('auth') ||
        message.includes('unauthorized')
      ) {
        return this.classifications.get(
          ErrorType.AUTHENTICATION_ERROR
        )!;
      }
    }

    return {
      type: ErrorType.UNKNOWN_ERROR,
      retryable: true,
      baseDelay: 1000,
      maxRetries: 3,
    };
  }
}

具体例

出力拘束の実装例

実際のプロジェクトで活用できる、完全な出力拘束システムの実装を示します。この例では、商品カタログの生成を想定したユースケースを扱います。

typescriptinterface Product {
  id: number;
  name: string;
  category: string;
  price: number;
  inStock: boolean;
  description: string;
  tags: string[];
}

class ProductCatalogGenerator {
  private promptBuilder: ConstraintedPromptBuilder;

  constructor() {
    this.promptBuilder = new ConstraintedPromptBuilder()
      .setSchema({
        type: 'object',
        properties: {
          products: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                id: { type: 'number' },
                name: { type: 'string' },
                category: { type: 'string' },
                price: { type: 'number' },
                inStock: { type: 'boolean' },
                description: { type: 'string' },
                tags: {
                  type: 'array',
                  items: { type: 'string' },
                },
              },
              required: [
                'id',
                'name',
                'category',
                'price',
                'inStock',
                'description',
                'tags',
              ],
            },
          },
        },
      })
      .addConstraint('商品IDは1以上の整数')
      .addConstraint('価格は0以上の数値(税抜き)')
      .addConstraint('在庫状況はtrueまたはfalseのみ')
      .addConstraint('タグは最大5個まで')
      .addExample({
        products: [
          {
            id: 1,
            name: 'ワイヤレスイヤホン',
            category: '電子機器',
            price: 8980,
            inStock: true,
            description: '高音質なワイヤレスイヤホンです。',
            tags: ['audio', 'wireless', 'portable'],
          },
        ],
      });
  }

  async generateProducts(
    category: string,
    count: number
  ): Promise<Product[]> {
    const userPrompt = `
${category}カテゴリの商品を${count}個生成してください。
各商品は実在しそうな名前と適切な価格設定にしてください。
`;

    const fullPrompt = this.promptBuilder.build(userPrompt);

    try {
      const result = await this.callGeminiAPI(fullPrompt);
      const parsedResult = JSON.parse(result);
      return parsedResult.products;
    } catch (error) {
      throw new Error(`商品生成に失敗しました: ${error}`);
    }
  }

  private async callGeminiAPI(
    prompt: string
  ): Promise<string> {
    // Gemini API呼び出しの実装
    // 実際の実装では、APIキーやエンドポイントの設定が必要
    return ''; // プレースホルダー
  }
}

スキーマ検証コードの実装

前述の Product カタログ生成と連携した、包括的な検証システムの実装例です。

typescriptclass ProductCatalogValidator {
  private validator: JSONValidator;
  private repairService: JSONRepairService;
  private productSchema: JSONSchemaType<{
    products: Product[];
  }>;

  constructor() {
    this.validator = new JSONValidator();
    this.repairService = new JSONRepairService();

    this.productSchema = {
      type: 'object',
      properties: {
        products: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              id: { type: 'integer', minimum: 1 },
              name: {
                type: 'string',
                minLength: 1,
                maxLength: 100,
              },
              category: { type: 'string', minLength: 1 },
              price: { type: 'number', minimum: 0 },
              inStock: { type: 'boolean' },
              description: {
                type: 'string',
                minLength: 1,
                maxLength: 500,
              },
              tags: {
                type: 'array',
                items: { type: 'string' },
                maxItems: 5,
              },
            },
            required: [
              'id',
              'name',
              'category',
              'price',
              'inStock',
              'description',
              'tags',
            ],
            additionalProperties: false,
          },
          maxItems: 100,
        },
      },
      required: ['products'],
      additionalProperties: false,
    };
  }

  async validateProductCatalog(rawData: string): Promise<{
    valid: boolean;
    products?: Product[];
    errors: string[];
    warnings: string[];
  }> {
    const result =
      await this.repairService.validateAndRepair(
        rawData,
        this.productSchema
      );

    if (!result.success) {
      return {
        valid: false,
        errors: result.errors,
        warnings: [],
      };
    }

    // ビジネスロジック検証
    const businessValidation = this.validateBusinessRules(
      result.data!.products
    );

    return {
      valid: businessValidation.valid,
      products: result.data!.products,
      errors: result.errors,
      warnings: businessValidation.warnings,
    };
  }

  private validateBusinessRules(products: Product[]): {
    valid: boolean;
    warnings: string[];
  } {
    const warnings: string[] = [];

    // 重複ID検証
    const ids = products.map((p) => p.id);
    const duplicateIds = ids.filter(
      (id, index) => ids.indexOf(id) !== index
    );
    if (duplicateIds.length > 0) {
      warnings.push(
        `重複する商品ID: ${duplicateIds.join(', ')}`
      );
    }

    // 価格妥当性検証
    products.forEach((product) => {
      if (product.price > 1000000) {
        warnings.push(
          `商品「${product.name}」の価格が高額すぎます: ${product.price}円`
        );
      }
      if (product.price < 1 && product.inStock) {
        warnings.push(
          `在庫ありなのに価格が0円の商品があります: ${product.name}`
        );
      }
    });

    // カテゴリ一貫性検証
    const categories = [
      ...new Set(products.map((p) => p.category)),
    ];
    if (categories.length > 5) {
      warnings.push(
        `カテゴリ数が多すぎます(${categories.length}個)。一貫性を確認してください。`
      );
    }

    return {
      valid: duplicateIds.length === 0, // 重複IDは致命的エラー
      warnings,
    };
  }
}

再試行ロジックの実装

出力拘束とスキーマ検証を組み合わせた、完全な再試行システムの実装例です。

typescriptclass RobustProductGenerator {
  private generator: ProductCatalogGenerator;
  private validator: ProductCatalogValidator;
  private retryManager: SmartRetryManager;
  private metrics: GenerationMetrics;

  constructor() {
    this.generator = new ProductCatalogGenerator();
    this.validator = new ProductCatalogValidator();
    this.retryManager = new SmartRetryManager();
    this.metrics = new GenerationMetrics();
  }

  async generateValidatedProducts(
    category: string,
    count: number
  ): Promise<{
    products: Product[];
    metrics: GenerationStats;
  }> {
    const startTime = Date.now();

    const operation = async (): Promise<Product[]> => {
      // 1. 商品生成
      const rawProducts =
        await this.generator.generateProducts(
          category,
          count
        );
      const rawJSON = JSON.stringify({
        products: rawProducts,
      });

      // 2. 検証
      const validation =
        await this.validator.validateProductCatalog(
          rawJSON
        );

      if (!validation.valid) {
        throw new Error(
          `Validation failed: ${validation.errors.join(
            ', '
          )}`
        );
      }

      // 3. 警告がある場合はメトリクスに記録
      if (validation.warnings.length > 0) {
        this.metrics.recordWarnings(validation.warnings);
      }

      return validation.products!;
    };

    try {
      const result =
        await this.retryManager.executeWithSmartRetry(
          operation
        );

      const stats: GenerationStats = {
        success: true,
        attempts: result.attempts,
        duration: Date.now() - startTime,
        errors: result.errors,
        warnings: this.metrics.getWarnings(),
        productCount: result.result.length,
      };

      return {
        products: result.result,
        metrics: stats,
      };
    } catch (error) {
      const stats: GenerationStats = {
        success: false,
        attempts:
          this.retryManager.getLastAttemptCount?.() || 0,
        duration: Date.now() - startTime,
        errors: [String(error)],
        warnings: [],
        productCount: 0,
      };

      throw new GenerationError(
        '商品生成に失敗しました',
        stats
      );
    }
  }
}

interface GenerationStats {
  success: boolean;
  attempts: number;
  duration: number;
  errors: string[];
  warnings: string[];
  productCount: number;
}

class GenerationMetrics {
  private warnings: string[] = [];

  recordWarnings(warnings: string[]): void {
    this.warnings.push(...warnings);
  }

  getWarnings(): string[] {
    return [...this.warnings];
  }

  reset(): void {
    this.warnings = [];
  }
}

class GenerationError extends Error {
  constructor(
    message: string,
    public readonly stats: GenerationStats
  ) {
    super(message);
    this.name = 'GenerationError';
  }
}

実用的な使用例

実際のアプリケーションでの使用方法を示す、完全な実装例です。

typescriptasync function main() {
  const robustGenerator = new RobustProductGenerator();

  try {
    console.log('商品カタログ生成を開始します...');

    const result =
      await robustGenerator.generateValidatedProducts(
        '電子機器',
        10
      );

    console.log('✅ 生成成功!');
    console.log(`商品数: ${result.products.length}`);
    console.log(`試行回数: ${result.metrics.attempts}`);
    console.log(`実行時間: ${result.metrics.duration}ms`);

    if (result.metrics.warnings.length > 0) {
      console.log('⚠️  警告:');
      result.metrics.warnings.forEach((warning) =>
        console.log(`  - ${warning}`)
      );
    }

    // 生成された商品を表示
    result.products.forEach((product) => {
      console.log(
        `📦 ${product.name} - ¥${product.price} (${
          product.inStock ? '在庫あり' : '在庫なし'
        })`
      );
    });
  } catch (error) {
    if (error instanceof GenerationError) {
      console.error('❌ 生成失敗:');
      console.error(`試行回数: ${error.stats.attempts}`);
      console.error(`エラー履歴:`);
      error.stats.errors.forEach((err) =>
        console.error(`  - ${err}`)
      );
    } else {
      console.error('予期しないエラー:', error);
    }
  }
}

// 環境に応じた設定の調整
if (process.env.NODE_ENV === 'production') {
  // 本番環境では厳格な検証
  console.log('本番環境モード:厳格な検証を実行');
} else {
  // 開発環境では寛容な設定
  console.log('開発環境モード:警告のみで処理続行');
}

main().catch(console.error);

図で理解できる要点:

  • 出力拘束、スキーマ検証、再試行が連携して動作する包括的なシステム
  • エラーの種類に応じた適応的な制御により、効率的な問題解決を実現
  • メトリクス収集により、システムの品質向上と運用監視を支援

まとめ

Gemini CLI の JSON 出力における信頼性問題は、適切な技術的アプローチによって根本的に解決することができます。本記事でご紹介した 3 つの手法を組み合わせることで、安定したシステム運用を実現できるでしょう。

出力拘束による予防的対策では、プロンプトレベルでの明確な指示によって問題の発生確率を大幅に削減できます。単純な「JSON で出力してください」という指示から、構造化された制約条件を明示することで、期待する形式での出力を確実に得られるようになります。

JSON スキーマによる厳密な検証は、生成されたデータの品質を保証する重要な砦となります。ajv ライブラリを活用した包括的な検証システムにより、データ型の不整合やスキーマ違反を確実に検出し、必要に応じて自動修復を行うことができます。

知的な再試行メカニズムにより、一時的な問題や API の制限による失敗に対して効率的に対処できます。エラーの種類を分類し、それぞれに適した再試行戦略を適用することで、システムの可用性を最大化できます。

これらの手法は個別に適用することも可能ですが、真の威力は組み合わせて使用した場合に発揮されます。包括的なシステムを構築することで、JSON 出力の信頼性を飛躍的に向上させ、安定したサービス提供を実現できるのです。

実装の際は、プロジェクトの要件や制約に応じて段階的に導入することをお勧めします。まずは基本的な出力拘束から始め、徐々にスキーマ検証や再試行メカニズムを追加していくことで、リスクを最小化しながら品質向上を図ることができます。

継続的な改善とモニタリングにより、システムの品質をさらに向上させていくことも忘れてはなりません。メトリクス収集と分析により、問題の傾向を把握し、より効果的な対策を講じていきましょう。

関連リンク