構造化出力(Structured Outputs)徹底比較 ─ OpenAI・Anthropic・Gemini 2026のJSONスキーマ対応

OpenAI・Anthropic・Google Geminiの構造化出力(Structured Outputs)を2026年GA版で徹底比較。strictモードの実装差、再帰スキーマ・Union型・formatの対応差、本番前evalsまで、関数呼び出し専門の視点から整理する。

構造化出力 3社比較 2026

更新日: 2026年7月21日

構造化出力(Structured Outputs)は、LLMの応答をJSONスキーマに強制的に準拠させる機能で、2026年7月時点でOpenAI・Anthropic・Googleの3社すべてがGA提供しているが、対応するスキーマキーワード・再帰対応・拒否ハンドリングには依然として大きな差がある。本記事は関数呼び出し(function calling)とevals(評価)を専門にしてきた立場から、3社の strict モード・output_config.formatresponseJsonSchema を実装レベルで比較し、どのユースケースでどのベンダーを選ぶべきか、本番投入前に何を検証すべきかを整理する。「JSON modeで十分」と判断してしまう前に、この差分を把握してほしい。

正直、この3社の差分は公式ドキュメントを横断して読まないと拾えない部分が多くて、私自身も直近3ヶ月で本番切り替えの際に何度もハマった。だからこそ、実装者の視点で「先にここだけ見ておけ」というポイントを一枚にまとめている。

  • OpenAIの response_format: {type: "json_schema", strict: true} は制約デコードによる完全準拠を保証し、refusal フィールドで安全性拒否も明示される。
  • Anthropic Claudeは2026年に output_config.format をGAしたが、再帰スキーマ($ref: "#")は非対応で、strict tool合計20個・union型パラメータ16個の上限がある。
  • Gemini 3系は responseJsonSchema(旧 responseSchema は非推奨)で再帰・anyOfminimum/maximum をサポートするが、未対応キーワードを無音で無視するため検証が必須。
  • 3社共通で minLength/maxLength・任意の pattern・複雑な allOf+$ref の組み合わせは不安定で、Pydantic/ZodのSDKヘルパーは自動的にdescriptionへ格上げして落とす。
  • 本番投入前には「スキーマ準拠率」と「意味的正確性」を分けて計測するevalsが必須。準拠率100%でも中身が間違っていることは普通に起こる。

Structured OutputsとJSON modeは何が違うのか

ここは最初に整理しておきたい。JSON mode(OpenAIの response_format: {type: "json_object"} やGeminiの旧 responseMimeType: "application/json")は、「構文的にJSONであること」しか保証しない。中身のキーや型はプロンプト任せで、抜けたり型が変わったりする。一方、Structured Outputsは提供したJSONスキーマに対して制約デコード(constrained decoding)を使い、トークン生成時点で違反する遷移を確率0にする。結果、スキーマ準拠率は理論上100%になる。

この違いはeval指標にそのまま出る。私が担当したケースでは、JSON modeで運用していたエンドポイントの実測準拠率は gpt-4o で約94%、外れ値はほとんどがenum違反と余計なキーの追加だった。同じスキーマを strict: true に切り替えるだけで準拠率100%まで到達し、リトライループを丸ごと削除できた(あの重い try/except を消せた瞬間は少し嬉しかった)。ただし後述するように、strictモードには許容されるスキーマの形に制約があり、既存の複雑なPydanticモデルをそのまま渡すと Invalid schema エラーで弾かれる。

もうひとつ重要な違いは拒否(refusal)の扱いだ。OpenAIは安全上の理由でモデルが生成を拒否した場合に message.refusal にメッセージが入る。この分岐を必ずハンドリングしないと、下流のパーサが空文字列で落ちる。Claudeにはこれに相当する専用フィールドはなく、stop_reason と本文で判定する。Geminiは finishReasonpromptFeedback.blockReason を確認する必要がある。ここは3社で挙動が異なるため、抽象化レイヤーを噛ませる場合でも「生レスポンスへの参照」は保持しておく設計にしたい。

3社の対応状況を1枚で見る比較表

2026年7月時点のGA版で、実装者が気にする観点だけに絞った比較表を先に示す。詳細は各セクションで根拠と一緒に説明する。「vendorをdisる」意図はなく、単に「このユースケースにはこの選択肢が刺さる」を判断するための材料と思ってほしい。JSONスキーマの対応差はドキュメントを横断して読まないと拾えないため、実装者が最初に見る一枚目としてブックマーク推奨。

観点OpenAI (strict: true)Anthropic Claude (output_config.format)Google Gemini (responseJsonSchema)
GA提供モデルgpt-5.6 系列 / gpt-4o-2024-08-06以降Sonnet 4.5 / Opus 4.5 / Haiku 4.5 以降Gemini 3.6-flash / 3.1-pro-preview 以降
再帰スキーマ ($ref: "#")対応非対応対応
anyOf / union対応対応(合計16パラメータまで)対応(深いネストは不安定)
minLength / maxLength / pattern非対応SDKがdescriptionへ移動非対応(minimum/maximum は可)
string format限定的10種類 (date-time, uuid, email, hostname, uri…)date/time系のみ
拒否検出message.refusal 明示専用フィールドなしpromptFeedback.blockReason
strict tool数上限制限なし(実質128以下)20 tools / 24 optional paramsプレビュー中は要注意
SDKヘルパーPydantic (parse()) / ZodPydantic (messages.parse()) / zodOutputFormat()Pydantic / Zod (GenAI SDK)
初回スキーマコンパイルあり(次回以降キャッシュ)あり透明

OpenAI Structured Outputs(strict: true)の実装と制約

OpenAIのStructured Outputsは、Chat Completionsでは response_format: {type: "json_schema", json_schema: {name, schema, strict: true}}、新しいResponses APIでは text.format にネストされる。ここで注意したいのが、Responses APIでは response_format ではなく text.format を使う点。SDKアップグレード時にここでハマるチームが多い(私も一度PRレビューで見逃した)。OpenAIの公式Structured Outputsガイドにサンプルが載っているので、Responses APIへの移行時は必ず確認してほしい。

from openai import OpenAI
from pydantic import BaseModel

class InvoiceLine(BaseModel):
    description: str
    quantity: int
    unit_price: float

class Invoice(BaseModel):
    invoice_id: str
    lines: list[InvoiceLine]
    total: float

client = OpenAI()

# Responses API + Pydantic parse helper
response = client.responses.parse(
    model="gpt-5.6",
    input=[
        {"role": "system", "content": "PDFから請求書を抽出する"},
        {"role": "user", "content": pdf_text},
    ],
    text_format=Invoice,
)

parsed = response.output_parsed  # Invoice or None (refusal時)
if response.output[0].content[0].type == "refusal":
    handle_refusal(response.output[0].content[0].refusal)

strictモードのスキーマ側の制約は3つ押さえておきたい。1つ目、すべてのオブジェクトで additionalProperties: false が必須。2つ目、すべてのプロパティを required に入れる必要がある(オプショナルはnull許容 union で表現する)。3つ目、ネストは実質5階層程度が上限として長らく共有されているが、公式ドキュメントは「パフォーマンス上の理由で一部の制約が使えない」と明記するのみで正確な数値は変動する。Pydantic v2の Optional[X] をそのまま渡すと required に含まれないため弾かれるので、Field(default=None) ではなく X | None のUnionで表現するのが実務上のパターン。

本番運用では finish_reason: "length"(トークン上限で切れた)と refusal の2分岐を必ずハンドルする。前者は max_output_tokens の設計ミスなのでスキーマの見直しよりも上限見直しが優先。後者は安全性ポリシーに触れているためスキーマではなくプロンプトを見直す。この2つを混同すると、無限リトライで課金だけが増える悲しい事態になる(先月の請求書を見て初めて気づく、あれ)。

Anthropic Claude output_config.formatのGA対応

Anthropicは長い間「function calling(tool use)の input_schema を経由してJSONを取り出す」パターンを推奨してきたが、2026年に Structured OutputsがGAとなり、パラメータ名も output_format から output_config.format にリネームされた(旧名も過渡期は動作する)。Claudeの構造化出力ドキュメントにモデル対応表と制約が集約されている。

import anthropic
from pydantic import BaseModel

class SentimentResult(BaseModel):
    sentiment: str  # positive / negative / neutral
    confidence: float
    key_phrases: list[str]

client = anthropic.Anthropic()

# Native SDK helper - Pydanticから自動でjson_schema化
response = client.messages.parse(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": review_text}],
    output_config={
        "format": {
            "type": "json_schema",
            "schema": SentimentResult,
            "strict": True,
        }
    },
)

result: SentimentResult = response.output_parsed

Claudeの制約は独特で、実務で刺さるのはこれら。再帰スキーマ非対応($ref: "#" 不可)、allOf + $ref 併用不可、minItems は0か1のみ、strict tool合計20個・オプショナル24個・union型16個の上限。pattern 正規表現はサポートされるが後方参照(\1)・ルックアラウンド・\b は禁止。enumのケース(大文字小文字)は保証されないので、比較は必ずcase-insensitiveで行う。SDKヘルパーはPydanticの未対応制約を自動的にdescriptionへ移動させて落とすため、「動くけど強制はされていない」状態になっている点は要注意。

Tool useとStructured Outputsは役割分担する設計に変わった。ツールに渡すパラメータの検証は今まで通り tools[].input_schemastrict: true、最終応答のシリアライズは output_config.format、と使い分ける。既存パイプラインでtool use経由で構造化していたコードは、そのままでも動くが、evalsとログの粒度を上げたいならStructured Outputsに寄せると監視しやすい。エージェント運用の全体像はClaude Agent SDKの本番運用ガイドで整理しているので、SDK側のフックと合わせて設計してほしい。

Google Gemini responseJsonSchemaの落とし穴

Geminiは2026年に responseSchema を非推奨化し、responseJsonSchema をメインAPIとして提供している(Gemini公式構造化出力ガイド参照)。ただし旧 responseSchemaresponseMimeType: "application/json" もバッチAPIや古いSDKでは現役で、ドキュメント間でも表記が揺れている。実装時はSDKバージョンとリファレンスページの日付を必ず突き合わせるのが安全。

from google import genai
from pydantic import BaseModel

class ProductReview(BaseModel):
    rating: int
    pros: list[str]
    cons: list[str]

client = genai.Client()

response = client.models.generate_content(
    model="gemini-3.6-flash",
    contents=review_text,
    config={
        "response_mime_type": "application/json",
        "response_json_schema": ProductReview.model_json_schema(),
    },
)

parsed = ProductReview.model_validate_json(response.text)

Geminiの最大の落とし穴は「未対応キーワードを無音で無視する」挙動だ。OpenAIやClaudeがスキーマ検証で早期に失敗させるのに対し、Geminiは未知の制約を静かに落として応答を返す。minLength を書いても長さ制約が効かず、後段でPydanticにかけて初めて弾かれる(このパターンで丸一日デバッグに溶かした経験あり)。propertyOrdering を明示しないと、Python以外のSDKでは必須プロパティ→アルファベット順で出力されるので、日付順に並べたいフィールドが逆順で出るなどのdrift事故が起こる。

Gemini 3系ではtoolsとStructured Outputsを同時利用できるが、これは3.1-preview以降の話であり、それ以前のモデルは組み合わせ不可。深いネスト(実務上6階層以上)と長いunionは失敗率が上がるので、evalsでスキーマ複雑度別の準拠率を測っておくと本番でハマらない。数値制約に強い一方で文字列制約が弱いというプロファイルは、金融データや在庫データの抽出には向くが、自由記述の正規化には別のバリデーション層が必要になる。

再帰スキーマ・Union型・formatの実測差

コメント木・ファイルシステム・AST抽出のようなユースケースでは再帰スキーマが必要になる。ここでClaudeだけが対応していないのは実務上大きな制約で、Claudeを使うなら深さ固定でスキーマを展開するしかない。私の運用では最大深度3までを許容する形で NodeDepth1 / NodeDepth2 / NodeDepth3 を明示的に定義してunionで束ねている。冗長だが、preprocessorで自動生成できるので保守負担は許容範囲。

Union型(anyOf / oneOf)については3社とも動くが、「差別化された(discriminated)union」かどうかで安定性が大きく変わる。以下のように type フィールドをdiscriminatorとして明示すると、OpenAI・Gemini双方で準拠率が体感で15〜20%上がる。Claudeは16パラメータ上限に注意しつつ、同じパターンが有効。

{
  "anyOf": [
    {"type": "object",
     "properties": {"type": {"const": "text"}, "content": {"type": "string"}},
     "required": ["type", "content"]},
    {"type": "object",
     "properties": {"type": {"const": "image"}, "url": {"type": "string"}},
     "required": ["type", "url"]}
  ]
}

formatキーワードの扱いはベンダー間の温度差が最も大きい。Claudeは date-time / date / time / duration / email / hostname / uri / ipv4 / ipv6 / uuid の10種類をネイティブに検証する。OpenAIとGeminiは限定的で、実質的にはPydanticやZodの後段バリデーションで補うことになる。日付を扱うプロダクトでClaudeを選ぶメリットのひとつ。逆にUUIDやIPアドレスの厳密検証を後段でしっかり書く覚悟があるならOpenAI/Geminiでも実務上は問題ない。JSON Schema自体の仕様はJSON Schema Validation draft 2020-12を参照すると各キーワードの意味が最短で理解できる。

本番前に走らせるべきevals — 準拠率と意味的正確性を分ける

「Structured Outputsだから100%動く」と信じて本番投入するのは危険だ。Structured Outputsが保証するのはスキーマ準拠率だけで、意味的正確性(内容が正しいか)は別問題。私が全プロジェクトで最低限走らせている評価は次の3つ。

  1. スキーマ準拠率(schema compliance rate): N回叩いてJSONパースが通り、Pydantic/Zodのバリデーションも通る割合。strictモードなら100%を期待するが、拒否(refusal)発生率は別カウントする。
  2. フィールド別正確性: ground truthとの一致率をフィールド単位で出す。全体マッチだけ見ると「rating以外は当たっているが全体不一致」ケースが埋もれる。
  3. 複雑度別回帰: スキーマの深さ・プロパティ数・union分岐数を軸に、複雑度が上がるほど正確性がどう落ちるかを可視化する。この曲線がベンダー選定の判断材料になる。

評価パイプラインの具体的な組み方はAIエージェントのEvals実践入門で決定論的チェックとLLM-as-a-Judgeの組み合わせを解説しているので、そちらと合わせて設計してほしい。「evalsをスキップしていい理由」は本番ではひとつも存在しない、というのが個人的な信条だ。

継続的な運用では、モデルバージョンが上がるたび(gpt-5.6 → 5.7、Sonnet 4.5 → Opus 5など)にこの3指標を再測定する。ベンダーは「後方互換」を謳うが、実測ではフィールド別正確性が5%以上ドリフトすることがある。CIに組み込んでおくと、夜間デプロイのあとも安眠できる。

Pydantic・Zod・Instructorでの実装パターン

スキーマファースト運用では、唯一のsource of truthを型定義側に置く。PythonならPydantic v2、TypeScriptならZod、というのが2026年時点のデファクト。SDKに直接JSON Schemaを渡すのではなく、モデル/型からSDKヘルパーが自動変換する形にする。理由は、モデル定義とスキーマの二重管理がすぐdriftを起こすからだ。

// TypeScript / Zod + Anthropic SDK
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";

const Ticket = z.object({
  priority: z.enum(["low", "medium", "high", "urgent"]),
  labels: z.array(z.string()).max(5),
  summary: z.string(),
});

const client = new Anthropic();
const res = await client.messages.parse({
  model: "claude-sonnet-4-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: userReport }],
  output_config: { format: zodOutputFormat(Ticket, "ticket") },
});

const ticket = res.output_parsed; // z.infer<typeof Ticket>

マルチベンダー抽象を導入したいならInstructor(Python / TS)が事実上の標準。リトライ・部分ストリーム・バリデーション統合が入っており、OpenAI/Anthropic/Geminiすべてを同じPydanticモデルで叩ける。ただし抽象化のコストとして、ベンダー固有の refusalpromptFeedback が隠蔽されるので、監査ログ用に生レスポンスも別途保存しておくのが実務パターン。

型安全なエージェント構築を全体像で見たい場合はPydanticAIで型安全なAIエージェントを構築する実践ガイドに依存性注入とツール定義まで含めた形で書いた。Structured OutputsとPydanticAIのアプローチはよく組み合わさるので、双方合わせて読むと本番設計しやすい。

よくある質問

Structured OutputsとJSON modeの違いは何ですか?

JSON modeは「構文的にJSON」を保証するだけで、キーや型はプロンプト任せです。Structured Outputsは提供したJSONスキーマに対して制約デコードを使い、生成時点でスキーマ違反を防ぎます。準拠率は理論上100%になり、リトライループを削除できます。

ClaudeにはOpenAIのstrict: true相当の機能はありますか?

あります。2026年に output_config.format(type: "json_schema", strict: true)がGAとなり、Sonnet 4.5 / Opus 4.5 / Haiku 4.5以降で利用可能です。ただし再帰スキーマ($ref: "#")は非対応で、strict tool合計20個・union型16個の上限があります。

再帰スキーマ($ref: "#")は3社でどれが動きますか?

OpenAIとGeminiは対応、Anthropic Claudeは非対応です。Claudeで木構造や再帰的なASTを扱う場合は、最大深度を固定して NodeDepth1 / NodeDepth2 / NodeDepth3 のように明示的に展開してunionで束ねる回避策が必要です。

Structured Outputsのレイテンシオーバーヘッドはどのくらいですか?

初回のスキーマコンパイル分だけ数百ミリ秒から1秒程度加算されることがありますが、同一スキーマの2回目以降はキャッシュされ気になりません。多数の動的スキーマを叩くユースケースでのみ実測すべきです。GeminiはこのコンパイルコストがSDKレベルで透明化されています。

ストリーミングしながらスキーマを強制できますか?

3社とも可能ですが、途中トークンは有効なJSON部分ではないため、クライアント側でバッファして最終完成後にパースするパターンが一般的です。部分パース(partial parse)が欲しい場合はInstructorライブラリの Partial[Model] ヘルパーが便利です。

PydanticやZodは各ベンダーSDKでネイティブサポートされていますか?

はい。OpenAI SDK(responses.parse)、Anthropic SDK(messages.parse / zodOutputFormat)、Google GenAI SDKすべてでPydantic/Zodからの自動変換ヘルパーが公式提供されています。Instructorを噛ませればマルチベンダー抽象が得られます。

Daichi Watanabe
著者について Daichi Watanabe

LLM integration specialist with a strong opinion about function calling and an even stronger one about evaluations.