構造化出力 (--json-schema)
モデルの最終回答を、指定した JSON Schema に制約します。Qwen Code は合成のターミナルツールを登録し、モデルはそれを呼び出す必要があります。ツール呼び出しの引数をスキーマに対してパースし、検証されたペイロードを標準出力 (もしくは JSON / stream-json の結果エンベロープ) に出力します。最初の有効な呼び出しで実行が終了します。
ヘッドレスモード専用 — qwen -p、位置引数によるプロンプト、または標準入力からのプロンプト入力と組み合わせて使用します。
クイックスタート
qwen --prompt "Summarize the changes in HEAD with risk_level" \
--json-schema '{
"type": "object",
"properties": {
"summary": { "type": "string" },
"risk_level": { "type": "string", "enum": ["low", "medium", "high"] }
},
"required": ["summary", "risk_level"],
"additionalProperties": false
}'標準出力への出力 (デフォルトの --output-format text):
{ "summary": "…", "risk_level": "low" }この行は、JSON 文字列化されたペイロード + 改行のみです — エンベロープもイベントログもありません。そのまま jq や他のコンシューマにパイプできます。
テキストモードでは、標準出力は成功時に JSON ペイロード用に予約され、失敗時は空になります。エラーメッセージとログ行は標準エラー出力に送られます。これにより、$(qwen --json-schema …) || exit 1 のようなキャプチャパターンはテキストモードでも安全に動作します — 失敗は標準エラー出力に出力され、キャプチャされた変数に混入しません。モデルが計画中に出力する付随的な散文は標準エラー出力にも反映されません — テキストモードでは破棄されます。それらを確認したい場合は --output-format json または stream-json を使用してください。
--output-format json および stream-json では、失敗結果メッセージも成功パスとともに標準出力に出力されます (JSON 配列の最後の要素、または JSONL ストリームの末尾の result 行として)。ただし、全ての失敗モードが結果を標準出力に出力するわけではありません — max-session-turns (終了コード 53) とシグナル割り込み (終了コード 130) は標準エラー出力のみで終了します。最初に終了コードを確認してください。結果オブジェクトの is_error で、結果イベントを生成する失敗の中での区別が可能です。
空のスキーマ:
{}を渡すと、標準出力に{}(空の JSON オブジェクト) が出力されます。モデルはstructured_outputを引数なしで呼び出します。上流の引数正規化パスによって空の関数呼び出しが空オブジェクトペイロードに変換され、空のスキーマに対する検証を通過してそのまま出力されます。
スキーマの指定
2 つの同等な形式:
# インラインの JSON リテラル
qwen -p "…" --json-schema '{"type":"object", "properties":{…}}'
# ファイルから読み取り
qwen -p "…" --json-schema @./schemas/summary.json@path 形式は ~ を展開し、パスを正規化し、utf8 エンコーディングでファイルを読み取ります。
レイテンシに関する注意: 成功した実行では、結果が出力される前に、実行中のバックグラウンドエージェントが最後の通知をフラッシュするために最大 ~500 ms のシャットダウンホールドバックが発生します。保留中のバックグラウンドタスクがない場合はホールドバックは早期に終了するため、単純な実行ではほとんど影響はありません。ビジーなエージェントに対して数百の
--json-schema呼び出しをファンアウトするバッチパイプラインでは、この上限を考慮する必要があります。
セキュリティに関する注意: スキーマには、
patternキーワードにユーザー提供の正規表現を含めることができます。Ajv はこれらを ECMAScript 正規表現エンジンでコンパイルするため、壊滅的なバックトラッキング (catastrophic backtracking) に対して脆弱です。ツールの引数は常にオブジェクトであるため、patternキーワードは文字列プロパティ内でのみ発動します。悪意のあるスキーマ ({"type":"object","properties":{"value":{"type":"string","pattern":"(a+)+b"}}}など) は、モデルが中程度の長さのマッチする値を提供した場合に CLI をハングさせる可能性があります。--json-schemaは、信頼できるソースからのスキーマでのみ実行してください。
パース時の検証:
- ファイルは通常のファイルである必要があります (FIFO、キャラクタデバイス、ディレクトリは不可)。
- ファイルサイズは 4 MiB に制限されています。現実の JSON スキーマはこれをはるかに下回ります。数 MiB のファイルは、ほとんどの場合、パス指定の誤りを示します。
- スキーマは有効な JSON である必要があります。
@path入力の場合、パースエラーは汎用的なメッセージ (“<path>のコンテンツは有効な JSON ではありません”) となり、SyntaxError の詳細は出力されません。これにより、標準エラー出力を参照するラッパープロセスがエラーからファイル内容のプレフィックスを読み取ることを防ぎます。 - スキーマは厳格な Ajv 設定でコンパイルできる必要があります —
properteesのようなタイプミスは報告されますが、仕様上有効なパターン (例:requiredにproperties内のすべてのキーを記載しない) は受け入れられます。 - スキーマのルートはオブジェクト型の値を受け入れる必要があります。関数呼び出し API (Gemini、OpenAI、Anthropic) はすべてツール引数が JSON オブジェクトであることを要求するため、非オブジェクトのルートは使用不可能なツールとして登録されます。
ルート受け入れチェックは、type、const、enum、anyOf、oneOf、allOf、not、if/then/else を確認します (決定可能なケースについてはベストエフォート)。不明な場合は実行時に Ajv に委ねます。
ルートの
$refはパース時チェックで拒否されます。 スキーマが$refを介して定義を再利用する場合は、allOfでラップしてください:// 拒否: { "$ref": "#/$defs/MyObj", "$defs": { "MyObj": { "type": "object", "properties": { "name": { "type": "string" } } } } } // 受け入れ (ルートが allOf ブランチを介してオブジェクトを受け入れる): { "allOf": [{ "$ref": "#/$defs/MyObj" }], "$defs": { "MyObj": { "type": "object", "properties": { "name": { "type": "string" } } } } }
anyOf/oneOf/allOf内部の$refは実行時に Ajv に委ねられるため、ラップされた形式はルート受け入れチェックを通過します。
フォーマットごとの出力形状
--output-format | 標準出力への出力内容 |
|---|---|
text (デフォルト) | JSON.stringify(payload) + "\n" — 1行、検証されたオブジェクト。 |
json | メッセージオブジェクトの単一の JSON 配列 (完全なイベントログ)。最後の要素は type: "result" メッセージで、result (JSON.stringify(payload)) と structured_result (生のオブジェクト) の両方を保持します。 |
stream-json | 各イベントが JSONL としてそれぞれの行に出力されます。終了する result 行は result (文字列化) と structured_result (生のオブジェクト) を保持します。 |
どちらの JSON 形式でも、オブジェクトが必要な場合は result ではなく structured_result を読むことを推奨します。result は、そのフィールドに常に文字列を期待するコンシューマのために提供される文字列化された形式です。--output-format json の場合は配列の最後の要素を読み取り、そこから structured_result を取得します (例: jq '.[-1].structured_result')。stream-json の場合はストリーム上の最後の type: "result" 行を読み取ります。
制限事項
| 組み合わせ | 動作 |
|---|---|
--json-schema + -i / --prompt-interactive | パース時に拒否されます。合成ツールの「セッションはここで終了します」というメッセージは TUI ループ内にターミネータを持ちません。 |
--json-schema + --input-format stream-json | パース時に拒否されます。シングルショットのターミナル契約は、長期実行される stream-json 入力プロトコルと互換性がありません。 |
--json-schema + --acp / --experimental-acp | パース時に拒否されます。ACP は独自のターンループを実行するため、合成ツールのターミナル契約を尊重しません。 |
--json-schema でプロンプトもパイプ入力もなし | パース時に拒否されます。ヘッドレスモードにはプロンプトが必要です — -p、位置引数、またはパイプ入力を指定してください。 |
--bare + --json-schema | サポートされています。合成ツールはベアの3つ (read_file、edit、run_shell_command) とともに登録されます。 |
サブエージェント内の --json-schema | ツールは登録されません。トップレベル実行のメイン/ドレインターンのみがターミナル契約を尊重します。サブエージェントがツールを呼び出すと「セッションはここで終了します」を受け取り、ループにターミネータがないため実行が継続されます。 |
リトライと障害モード
コストに関する注意.
--json-schema実行では、トークン消費を増やす2つの要因があり、どちらも設計時に考慮する価値があります:
- 毎ターンにスキーマが埋め込まれる。 スキーマは、最初のリクエストだけでなく、すべてのモデルリクエストにおいて
structured_output関数宣言のparametersブロックとして送信されます。大きなスキーマ (最大 4 MiB のパース上限まで) は、実行全体のターンごとの入力トークンを比例して増加させます。- 検証リトライごとに完全なモデルターンが1回発生する。 モデルが繰り返し間違えるスキーマは、失敗ごとに乗算されます (リクエスト + 推論 + 応答)。モデルを導くのに十分な制約があり、かつ最初の試行で成功するほどシンプルなスキーマに抑えてください。リトライが予想される場合は
--max-session-turnsを増やしてください。
セッションは最初の有効な呼び出しで終了します。それまでは:
- 引数が検証に失敗する。
structured_outputは Ajv のメッセージを含むツール結果エラーを返します。モデルは次のターンでそれを確認し、引数を修正してもう一度呼び出す可能性があります。 - モデルが
structured_outputと同じターンで副作用のあるツールを呼び出す。 プリスキャンはその兄弟ツールを抑制します — 構造化呼び出しが最終的に検証に成功するかどうかに関わらず、決して実行されません。2つのパスは、モデルが次に見るものに応じて分岐します:- 検証成功: 実行は即座に終了し、モデルは次のターンを得られません — 抑制された兄弟ツールは静かに破棄されます。
- 検証失敗: モデルは次のターンを得て、抑制された呼び出しに対して合成された “Skipped:”
tool_resultを確認するため、その呼び出しを別のターン (ただしstructured_outputを含まないターン) で再発行できます。
- モデルが
structured_outputを呼び出さずにプレーンテキストを出力する。 終了コード1。エラーメッセージにはターン数とモデル出力の切り詰められたプレビューが含まれるため、モデルが実際に何を言ったかを確認できます。 - 実行が
maxSessionTurnsに達する。 終了コード53。標準の “Reached max session turns” 終了に加えて、--json-schema固有のヒントが含まれ、よくある3つのスタック原因 (モデルがツールを呼び出さなかった、structured_outputが権限ルールで拒否された、スキーマが充足不可能) を指摘します。 - 実行が中断される (SIGINT / Ctrl-C)。 終了コード
130。通常は構造化結果は出力されませんが、シャットダウンホールドバックループはアボートシグナルをポーリングしないため、成功した呼び出しがキャプチャされた後、結果が標準出力に到達する前に SIGINT が到着した場合、結果が標準出力に出力される可能性があります。終了コードを信頼できる情報源として扱ってください。
プライバシー
structured_output を通じて送信する引数は、構造化ペイロードそのものです — 既に標準出力に出力されています。同じペイロードが再度、マシンからエクスポートされる可能性のあるオンデバイス上のインターフェースに永続化されるのを防ぐため、引数は以下の場所でプレースホルダー { __redacted: 'structured_output payload (see stdout result)' } に置き換えられます:
ToolCallEventテレメトリパス (OTLP エクスポート、QwenLogger、ui-telemetry ストリーム、チャット記録 UI イベントミラー)。- ディスク上のチャット記録 JSONL (
~/.qwen/projects/<sanitized-cwd>/chats/<sessionId>.jsonl、--continue/--resume時にモデルコンテキストに再投入される)。検証失敗のリトライもすべて含まれます。
ツール呼び出しのメトリクス (期間、成功、決定) および周囲のイベントメタデータは保持されます。
スキーマはモデルプロバイダーに送信されます。 リダクションはローカルサーフェス上の_呼び出し引数_のみを対象としています。スキーマ自体は、
structured_output関数宣言のparametersブロックとしてすべてのモデルリクエストに乗って送信されるため、スキーマ内に配置したリテラル値 (enum、const、default、examples、description、$commentなど) はプロンプトテキストと同様に平文でプロバイダーに到達します。スキーマは形状と制約を記述するものとして扱い、秘密情報、顧客記録、その他の機密ペイロードをスキーマ本文に含めないでください。
フックは生の引数を参照します。 上記のリダクションはテレメトリとチャット記録にのみ適用されます。
PreToolUse、PostToolUse、PostToolUseFailureフック (ペイロードをオフデバイスに転送できる HTTP フックを含む) は、structured_outputのリダクションされていないtool_inputを受け取ります。これは、フック契約が「ツールが見るものを見る」であるためです。監査スタイルのキャッチオールフックを運用している場合は、structured_outputに対してフックを無効にするか (tool_nameでフィルター)、機密データに対して--json-schemaを実行する前にフック側でリダクションを追加してください。
セッション再開 (--continue / --resume)
--json-schema は実行ごとのフラグであり、セッションごとのプロパティではありません。合成ツールは CLI が引数をパースするときに登録されます。つまり:
--continue/--resumeでターミナル契約を適用したい場合は、毎回--json-schemaを再指定してください。元の実行と同じスキーマが安全なデフォルトです — セッション途中でのスキーマの変更は可能ですが、モデルが準拠する契約が変わります。--json-schemaなしで--continueした場合、再開された実行は通常のヘッドレスセッションになります:structured_outputはツールとして存在せず、モデルは自由形式のテキストで応答します。- 再開されたチャット記録内の
__redactedプレースホルダーは、実際には再開可能性に影響しません。structured_outputの呼び出しが成功するとセッションは即座に終了するため、再開された実行が見ることができるリダクションされた引数は失敗した試行からのものだけです。モデルは、各試行の Ajv 検証エラーを記録されたtool_resultに、ライブのパラメータスキーマ (--json-schemaから再登録されたもの) を保持しているため、リトライには十分です。
権限ゲート
structured_output は意図的に --core-tools 許可リストをバイパスします: このツールは --json-schema が設定されている場合にのみ存在するため、除外すると実行にターミナル契約がなくなってしまいます。
明示的な permissions.deny ルールと --exclude-tools 設定は有効です — どちらも同じ拒否メカニズムを使用し、structured_output が登録されるのを防ぐため、モデルはツール宣言を参照しません。典型的な結果として、モデルはプレーンテキストで応答します (終了コード 1)。モデルがテキストを生成せずに他のツールをループし続けた場合、最終的に maxSessionTurns (終了コード 53) に達し、エラーメッセージ内の --json-schema ヒントが確認すべき場所を示します。
--bareの注意点。 ベアモードは、設定レベルのpermissions.denyやtools.excludeを含む、ほとんどの設定由来の入力を無視します。合成ツールは登録されたままになるため、structured_outputの設定のみによる拒否は--bareでは静かに効果がなくなります。argv レベルの--exclude-tools structured_outputはベアモードでも適用されます — ベア実行をロックダウンする必要がある場合は、設定ではなくフラグを使用してください。
MCP ツールとの競合
MCP サーバーが文字通り structured_output という名前のツールを登録した場合、ツールレジストリの衝突チェックにより MCP ツールは mcp__<server-name>__structured_output にリネームされ、合成ツールが元の名前を保持します。ユーザーが指定したスキーマが常にモデルに見えるものになります。
例: 構造化出力でマルチステップ実行をゲートする
RESULT=$(qwen --prompt "Audit this diff and rate its risk." \
--json-schema @./schemas/audit.json) || exit 1
risk=$(jq -r '.risk_level' <<<"$RESULT")
if [ "$risk" = "high" ]; then
echo "High-risk diff; pausing pipeline." >&2
exit 2
fi