T-CREATOR

【解決策】GPT-5 構造化出力が崩れる問題を直す:JSON モード/スキーマ厳格化の実践手順

【解決策】GPT-5 構造化出力が崩れる問題を直す:JSON モード/スキーマ厳格化の実践手順

GPT-5 の構造化出力で期待した形式の JSON が返ってこない、スキーマ違反でエラーが頻発する、といった問題に悩まされていませんか。これらの問題は適切な設定とスキーマ厳格化により解決できます。今回は、GPT-5 の構造化出力を安定させる具体的な解決策をステップバイステップでお伝えします。

背景

GPT-5 の構造化出力機能とは

GPT-5 では、従来のテキスト生成に加えて、指定された形式での構造化出力が可能になりました。これにより、JSON、XML、CSV などの特定のデータ形式で結果を取得できるようになっています。

構造化出力の主な特徴は以下の通りです。

json{
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "structured_output",
      "schema": {
        "type": "object",
        "properties": {
          "result": { "type": "string" },
          "confidence": { "type": "number" }
        },
        "required": ["result", "confidence"]
      }
    }
  }
}

この機能により、API レスポンスを直接アプリケーションのデータ構造に組み込むことができるようになりました。

従来の出力制御との違い

従来の GPT モデルでは、プロンプトエンジニアリングによる出力制御が主流でした。しかし、この方法には限界がありました。

従来の方法の問題点を図で示します。

mermaidflowchart TD
  prompt[プロンプト設計] --> gpt[GPT モデル]
  gpt --> text[テキスト出力]
  text --> parse[手動パース処理]
  parse --> validate[バリデーション]
  validate --> error[エラー処理]
  error --> retry[リトライ処理]

  style error fill:#ffcccb
  style retry fill:#ffcccb

従来の方法では、出力されたテキストを手動でパースし、期待する形式かどうかを検証する必要がありました。これに対して、GPT-5 の構造化出力では、スキーマレベルでの制御が可能です。

構造化出力が重要な理由

現代のアプリケーション開発において、API 間でのデータ交換は型安全性が求められます。構造化出力が重要な理由は以下の通りです。

項目従来の方法構造化出力
型安全性低い(文字列パース)高い(スキーマ準拠)
エラー発生率高い(パース失敗)低い(スキーマ検証)
開発効率低い(バリデーション実装)高い(自動検証)
メンテナンス性複雑(エラー処理)シンプル(型定義のみ)

特に TypeScript や Python の Pydantic を使用する場合、構造化出力により型定義との整合性を保ちながら開発を進められます。

課題

よくある出力崩れパターン

GPT-5 の構造化出力を使用する際に、以下のような問題が頻繁に発生します。これらは適切な設定により回避可能です。

最も一般的な問題パターンを整理しました。

typescript// 期待する形式
interface ExpectedOutput {
  name: string;
  age: number;
  skills: string[];
}

// 実際の出力(問題例)
{
  "name": "田中太郎",
  "age": "30",  // 文字列になってしまう
  "skills": "JavaScript, TypeScript"  // 配列ではなく文字列
}

このような型の不整合は、スキーマ定義の不備や、プロンプトの曖昧さが原因となることがほとんどです。

JSON スキーマ違反の典型例

スキーマ違反でよく見られるパターンをエラーコードとともに示します。

エラーコード: ValidationError: Schema validation failed

json// 定義したスキーマ
{
  "type": "object",
  "properties": {
    "items": {
      "type": "array",
      "items": { "type": "string" }
    },
    "total": { "type": "integer" }
  },
  "required": ["items", "total"]
}

// 問題のある出力
{
  "items": ["item1", "item2", null],  // null値が含まれる
  "total": 2.5  // 整数ではなく浮動小数点
}

このエラーは、スキーマでstring型を指定しているにも関わらず、null値が混入したことで発生します。

エラーが発生する具体的なケース

実際のプロジェクトで遭遇するエラーケースをコードとともに解説します。

ケース 1: ネストした構造での型不整合

javascript// API呼び出し
const response = await openai.chat.completions.create({
  model: 'gpt-5',
  messages: [
    { role: 'user', content: 'ユーザー情報を生成して' },
  ],
  response_format: {
    type: 'json_schema',
    json_schema: userSchema,
  },
});

// エラー: Expected object, got string
// 原因: ネストしたオブジェクトが文字列として返される

エラーメッセージ: TypeError: Cannot read properties of undefined (reading 'address')

このエラーは、ネストした構造の定義が不完全な場合に発生します。特に、オブジェクトの階層が深い場合に見られます。

ケース 2: 配列要素の型不整合

python# Python + Pydantic
from pydantic import BaseModel
from typing import List

class UserList(BaseModel):
    users: List[str]
    count: int

# エラーコード: ValidationError
# 1 validation error for UserList
# users.0: str type expected

これらのエラーは、配列内の要素型が期待したものと異なる場合に発生します。

解決策

JSON モードの適切な設定方法

GPT-5 で JSON 出力を確実に取得するには、以下の設定が必要です。まず基本的な設定から始めましょう。

typescriptimport { OpenAI } from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

次に、JSON モードの基本設定を行います。

typescriptconst completion = await openai.chat.completions.create({
  model: 'gpt-5',
  messages: [
    {
      role: 'system',
      content:
        'あなたは正確なJSON形式でのみ応答するアシスタントです。',
    },
    {
      role: 'user',
      content: 'ユーザー情報を生成してください',
    },
  ],
  response_format: { type: 'json_object' },
  temperature: 0.1, // 一貫性を高めるため低く設定
});

温度設定を低くすることで、出力の一貫性が向上し、スキーマ違反のリスクが減少します。

スキーマ厳格化の実装手順

より厳密な制御には、JSON Schema を使用します。段階的に実装していきましょう。

Step 1: 基本スキーマの定義

typescriptconst userSchema = {
  name: 'user_info',
  schema: {
    type: 'object',
    properties: {
      name: {
        type: 'string',
        minLength: 1,
        maxLength: 100,
      },
      age: {
        type: 'integer',
        minimum: 0,
        maximum: 150,
      },
      email: {
        type: 'string',
        format: 'email',
      },
    },
    required: ['name', 'age', 'email'],
    additionalProperties: false, // 予期しないプロパティを防ぐ
  },
};

additionalProperties: falseにより、定義されていないプロパティの出力を防げます。

Step 2: ネストした構造のスキーマ

typescriptconst complexSchema = {
  name: 'user_profile',
  schema: {
    type: 'object',
    properties: {
      personal: {
        type: 'object',
        properties: {
          name: { type: 'string' },
          age: { type: 'integer' },
        },
        required: ['name', 'age'],
      },
      skills: {
        type: 'array',
        items: { type: 'string' },
        minItems: 1,
        maxItems: 10,
      },
      experience: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            company: { type: 'string' },
            years: { type: 'integer' },
          },
          required: ['company', 'years'],
        },
      },
    },
    required: ['personal', 'skills'],
  },
};

ネストした構造では、各レベルでのrequiredプロパティの指定が重要です。

Step 3: API 呼び出しの実装

typescriptconst generateStructuredData = async (prompt: string) => {
  try {
    const completion = await openai.chat.completions.create(
      {
        model: 'gpt-5',
        messages: [
          {
            role: 'system',
            content:
              '指定されたスキーマに厳密に従ってJSON形式で応答してください。',
          },
          {
            role: 'user',
            content: prompt,
          },
        ],
        response_format: {
          type: 'json_schema',
          json_schema: complexSchema,
        },
        temperature: 0.1,
      }
    );

    return JSON.parse(
      completion.choices[0].message.content
    );
  } catch (error) {
    console.error('構造化出力エラー:', error);
    throw error;
  }
};

エラーハンドリングの最適化

構造化出力では、適切なエラーハンドリングが成功率を大きく左右します。

TypeScript での型安全なエラーハンドリング

typescriptimport { z } from 'zod';

// Zodスキーマの定義
const UserSchema = z.object({
  name: z.string().min(1).max(100),
  age: z.number().int().min(0).max(150),
  email: z.string().email(),
  skills: z.array(z.string()).min(1).max(10),
});

type User = z.infer<typeof UserSchema>;

次に、バリデーション機能付きの呼び出し関数を実装します。

typescriptconst generateValidatedUser = async (
  prompt: string
): Promise<User> => {
  let attempts = 0;
  const maxAttempts = 3;

  while (attempts < maxAttempts) {
    try {
      const completion =
        await openai.chat.completions.create({
          model: 'gpt-5',
          messages: [
            {
              role: 'system',
              content: `以下のスキーマに厳密に従ってJSONを生成してください。
            必要なフィールド: name (文字列), age (整数), email (メール形式), skills (文字列配列)`,
            },
            { role: 'user', content: prompt },
          ],
          response_format: { type: 'json_object' },
          temperature: 0.1,
        });

      const rawData = JSON.parse(
        completion.choices[0].message.content
      );
      const validatedData = UserSchema.parse(rawData);

      return validatedData;
    } catch (error) {
      attempts++;

      if (error instanceof z.ZodError) {
        console.warn(
          `バリデーションエラー (試行 ${attempts}/${maxAttempts}):`,
          error.errors
            .map((e) => `${e.path.join('.')}: ${e.message}`)
            .join(', ')
        );
      } else {
        console.warn(
          `API エラー (試行 ${attempts}/${maxAttempts}):`,
          error.message
        );
      }

      if (attempts >= maxAttempts) {
        throw new Error(
          `${maxAttempts}回の試行後も有効なデータを生成できませんでした`
        );
      }

      // 指数バックオフでリトライ
      await new Promise((resolve) =>
        setTimeout(resolve, Math.pow(2, attempts) * 1000)
      );
    }
  }
};

このエラーハンドリングにより、一時的な問題による失敗を回避し、成功率を向上させることができます。

具体例

基本的な JSON 出力の実装

実際のプロジェクトで使用できる基本的な実装例をご紹介します。まずは、シンプルなユーザー情報生成から始めましょう。

typescriptinterface BasicUser {
  id: string;
  name: string;
  role: 'admin' | 'user' | 'guest';
  isActive: boolean;
}

対応する JSON Schema を定義します。

typescriptconst basicUserSchema = {
  name: 'basic_user',
  schema: {
    type: 'object',
    properties: {
      id: {
        type: 'string',
        pattern: '^[a-zA-Z0-9-]+$',
        minLength: 5,
        maxLength: 20,
      },
      name: {
        type: 'string',
        minLength: 2,
        maxLength: 50,
      },
      role: {
        type: 'string',
        enum: ['admin', 'user', 'guest'],
      },
      isActive: { type: 'boolean' },
    },
    required: ['id', 'name', 'role', 'isActive'],
    additionalProperties: false,
  },
};

実装例を示します。

typescriptconst createUser = async (
  userPrompt: string
): Promise<BasicUser> => {
  const completion = await openai.chat.completions.create({
    model: 'gpt-5',
    messages: [
      {
        role: 'system',
        content:
          'ユーザー情報を指定されたJSON形式で生成してください。idは英数字とハイフンのみ使用し、roleは admin/user/guest のいずれかを選択してください。',
      },
      {
        role: 'user',
        content: userPrompt,
      },
    ],
    response_format: {
      type: 'json_schema',
      json_schema: basicUserSchema,
    },
    temperature: 0.2,
  });

  return JSON.parse(
    completion.choices[0].message.content
  ) as BasicUser;
};

// 使用例
const newUser = await createUser(
  '管理者権限を持つアクティブなユーザーを作成してください'
);
console.log(newUser);
// 出力例: { id: "admin-001", name: "山田太郎", role: "admin", isActive: true }

複雑なネストした構造の処理

次に、より複雑なデータ構造を扱う例をご紹介します。EC サイトの商品情報を想定した実装です。

typescriptinterface Product {
  id: string;
  name: string;
  price: {
    amount: number;
    currency: string;
  };
  categories: string[];
  specifications: {
    [key: string]: string | number | boolean;
  };
  availability: {
    inStock: boolean;
    quantity: number;
    estimatedShipping: string;
  };
}

複雑なスキーマの定義を段階的に行います。

typescriptconst productSchema = {
  name: 'product_info',
  schema: {
    type: 'object',
    properties: {
      id: {
        type: 'string',
        pattern: '^PRD-[0-9]{6}$',
      },
      name: {
        type: 'string',
        minLength: 3,
        maxLength: 100,
      },
      price: {
        type: 'object',
        properties: {
          amount: {
            type: 'number',
            minimum: 0,
            multipleOf: 0.01,
          },
          currency: {
            type: 'string',
            enum: ['JPY', 'USD', 'EUR'],
          },
        },
        required: ['amount', 'currency'],
      },
      categories: {
        type: 'array',
        items: { type: 'string' },
        minItems: 1,
        maxItems: 5,
        uniqueItems: true,
      },
      specifications: {
        type: 'object',
        patternProperties: {
          '^[a-zA-Z][a-zA-Z0-9_]*$': {
            oneOf: [
              { type: 'string' },
              { type: 'number' },
              { type: 'boolean' },
            ],
          },
        },
        additionalProperties: false,
      },
      availability: {
        type: 'object',
        properties: {
          inStock: { type: 'boolean' },
          quantity: {
            type: 'integer',
            minimum: 0,
          },
          estimatedShipping: {
            type: 'string',
            pattern: '^[0-9]{1,2}-[0-9]{1,2}日$',
          },
        },
        required: [
          'inStock',
          'quantity',
          'estimatedShipping',
        ],
      },
    },
    required: [
      'id',
      'name',
      'price',
      'categories',
      'specifications',
      'availability',
    ],
  },
};

実装とエラーハンドリングを含む完全な例です。

typescriptconst generateProduct = async (
  productDescription: string
): Promise<Product> => {
  try {
    const completion = await openai.chat.completions.create(
      {
        model: 'gpt-5',
        messages: [
          {
            role: 'system',
            content: `商品情報を以下の要件に従って生成してください:
          - IDは "PRD-" で始まる6桁の数字
          - 価格は小数点以下2桁まで
          - カテゴリは1-5個の重複しない文字列
          - 仕様は任意のキー(英数字とアンダースコア)と値(文字列、数値、真偽値)
          - 配送予定は "X-Y日" の形式`,
          },
          {
            role: 'user',
            content: productDescription,
          },
        ],
        response_format: {
          type: 'json_schema',
          json_schema: productSchema,
        },
        temperature: 0.3,
      }
    );

    const productData = JSON.parse(
      completion.choices[0].message.content
    );

    // 追加バリデーション
    if (productData.price.amount <= 0) {
      throw new Error(
        '価格は0より大きい値である必要があります'
      );
    }

    return productData as Product;
  } catch (error) {
    console.error('商品生成エラー:', error);
    throw new Error(
      `商品情報の生成に失敗しました: ${error.message}`
    );
  }
};

// 使用例
const product = await generateProduct(
  '高性能なBluetoothヘッドフォン、ノイズキャンセリング機能付き'
);
console.log(product);

実際のプロジェクトでの活用事例

最後に、実際の Web アプリケーションでの活用例をご紹介します。これは、ブログ記事の自動生成システムでの実装です。

プロジェクト構成の概要図

mermaidflowchart LR
  user[ユーザー] --> frontend[Next.js Frontend]
  frontend --> api[API Route]
  api --> gpt[GPT-5 API]
  gpt --> validation[Zod Validation]
  validation --> db[(Database)]
  db --> frontend

  style gpt fill:#e1f5fe
  style validation fill:#f3e5f5

この構成により、ユーザーの入力から構造化された記事データまでを一貫して処理できます。

API Route の実装 (​/​api​/​generate-article.ts)

typescriptimport { NextApiRequest, NextApiResponse } from 'next';
import { z } from 'zod';
import { OpenAI } from 'openai';

const ArticleSchema = z.object({
  title: z.string().min(10).max(100),
  summary: z.string().min(50).max(300),
  content: z
    .array(
      z.object({
        type: z.enum([
          'paragraph',
          'heading',
          'list',
          'code',
        ]),
        content: z.string(),
        level: z.number().int().min(1).max(6).optional(),
      })
    )
    .min(3),
  tags: z.array(z.string()).min(1).max(10),
  estimatedReadTime: z.number().int().min(1),
  seo: z.object({
    metaDescription: z.string().min(120).max(160),
    keywords: z.array(z.string()).min(3).max(10),
  }),
});

API ハンドラーの実装です。

typescriptexport default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res
      .status(405)
      .json({ error: 'Method not allowed' });
  }

  const { topic, targetAudience, tone } = req.body;

  const openai = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY,
  });

  const articleSchema = {
    name: 'blog_article',
    schema: {
      type: 'object',
      properties: {
        title: {
          type: 'string',
          minLength: 10,
          maxLength: 100,
        },
        summary: {
          type: 'string',
          minLength: 50,
          maxLength: 300,
        },
        content: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              type: {
                type: 'string',
                enum: [
                  'paragraph',
                  'heading',
                  'list',
                  'code',
                ],
              },
              content: { type: 'string' },
              level: {
                type: 'integer',
                minimum: 1,
                maximum: 6,
              },
            },
            required: ['type', 'content'],
          },
          minItems: 3,
        },
        tags: {
          type: 'array',
          items: { type: 'string' },
          minItems: 1,
          maxItems: 10,
        },
        estimatedReadTime: { type: 'integer', minimum: 1 },
        seo: {
          type: 'object',
          properties: {
            metaDescription: {
              type: 'string',
              minLength: 120,
              maxLength: 160,
            },
            keywords: {
              type: 'array',
              items: { type: 'string' },
              minItems: 3,
              maxItems: 10,
            },
          },
          required: ['metaDescription', 'keywords'],
        },
      },
      required: [
        'title',
        'summary',
        'content',
        'tags',
        'estimatedReadTime',
        'seo',
      ],
    },
  };

  try {
    const completion = await openai.chat.completions.create(
      {
        model: 'gpt-5',
        messages: [
          {
            role: 'system',
            content: `あなたは優秀なブログライターです。指定されたトピックについて、構造化された記事を生成してください。
          - 対象読者: ${targetAudience}
          - 文体: ${tone}
          - SEOを意識した内容にしてください
          - 読みやすい構造で作成してください`,
          },
          {
            role: 'user',
            content: `トピック: ${topic}`,
          },
        ],
        response_format: {
          type: 'json_schema',
          json_schema: articleSchema,
        },
        temperature: 0.7,
      }
    );

    const rawArticle = JSON.parse(
      completion.choices[0].message.content
    );
    const validatedArticle =
      ArticleSchema.parse(rawArticle);

    // データベースに保存(例)
    // await saveArticle(validatedArticle);

    res.status(200).json({
      success: true,
      article: validatedArticle,
    });
  } catch (error) {
    console.error('記事生成エラー:', error);

    if (error instanceof z.ZodError) {
      res.status(400).json({
        error: 'バリデーションエラー',
        details: error.errors,
      });
    } else {
      res.status(500).json({
        error: '記事生成に失敗しました',
      });
    }
  }
}

このように実装することで、一貫した構造の記事データを生成し、型安全性を保ちながらアプリケーションに統合できます。

まとめ

GPT-5 の構造化出力問題は、適切なスキーマ設計とエラーハンドリングにより確実に解決できます。重要なポイントをまとめます。

解決策の要点

  • JSON Schema の厳格な定義により型安全性を確保
  • Zod などのバリデーションライブラリとの組み合わせ
  • リトライ機構とエラーハンドリングの実装
  • 温度設定の最適化による出力の一貫性向上

実装時の注意点

  • additionalProperties: falseによる予期しないプロパティの防止
  • 配列やネストしたオブジェクトでの詳細な型指定
  • enum 値による選択肢の制限
  • 適切なバリデーションルールの設定

これらの手法を活用することで、GPT-5 の構造化出力を安定して利用でき、アプリケーションの信頼性を大幅に向上させることができます。構造化出力の問題に悩まされることなく、AI 機能をプロダクションで活用していきましょう。

関連リンク