構造化出力 (--json-schema)
指定した JSON Schema にモデルの最終回答を制約します。Qwen Code はモデルが呼び出すことを要求される合成ターミナルツールを登録し、 そのツール呼び出しの引数をスキーマに対して検証し、 検証済みのペイロードを stdout(または JSON / stream-json の結果エンベロープ)に出力します。最初に有効な呼び出しが行われた時点でセッションは終了します。
ヘッドレスモード専用 — qwen -p、位置引数によるプロンプト、または stdin 経由でパイプされたプロンプトで動作します。
クイックスタート
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
}'stdout への出力(デフォルト --output-format text):
{ "summary": "…", "risk_level": "low" }この行は JSON 文字列化されたペイロードそのもの + 改行であり、
エンベロープやイベントログは含まれません。そのまま jq や他のコンシューマーにパイプできます。
text モードでは、stdout は成功時の JSON ペイロード専用となり、失敗時は空になります。エラーメッセージとログは stderr に出力されます。
そのため、$(qwen --json-schema …) || exit 1 のようなキャプチャパターンは
text モードで安全に使用できます — 失敗は stderr に出力され、キャプチャされた変数に混入しません。また、プランニング中のモデルの散文的な出力も
stderr にはミラーされません — text モードではそれを破棄します。必要な場合は
--output-format json または stream-json を使用してください。
--output-format json および stream-json では、失敗時の結果メッセージは
成功パスと同様に stdout に出力されます(JSON 配列の最後の要素、または JSONL ストリームの終端の result 行として)。すべての失敗モードが stdout に結果を出力するわけではありません —
max-session-turns(終了コード 53)やシグナル割り込み(終了コード 130)では
stderr 出力のみで終了します。まず終了コードを確認し、結果イベントが生成される失敗の中での判別には is_error を使用してください。
空のスキーマ:
{}を渡すと stdout に{}(空の 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 正規表現エンジンでコンパイルしますが、カタストロフィックなバックトラッキングに対して脆弱です。ツールの引数は常にオブジェクトであるため、patternキーワードは文字列プロパティの内部でのみ機能します。悪意のあるスキーマ(例:{"type":"object","properties":{"value":{"type":"string","pattern":"(a+)+b"}}})では、モデルがある程度長いマッチする値を渡すと CLI がハングする可能性があります。--json-schemaは信頼できるソースからのスキーマのみで使用してください。
パース時の検証:
- ファイルは通常ファイルである必要があります(FIFO、キャラクターデバイス、ディレクトリは不可)。
- ファイルサイズは 4 MiB に制限されています。実際の JSON スキーマはこれよりはるかに小さいため、数 MiB のファイルはほぼ確実にパスの間違いを示します。
- スキーマは有効な JSON である必要があります。
@path入力の場合、パースエラーはジェネリックなメッセージ("content of '<path>' is not valid JSON")となり、SyntaxError の詳細はエコーされません。そのため、stderr をキャプチャするラッパープロセスはエラーからファイル内容の一部を読み取ることができません。 - スキーマは厳格な Ajv 設定でコンパイル可能である必要があります —
properteesのようなタイポは検出されますが、properties内のすべてのキーをrequiredに列挙しないようなスペック準拠のパターンは受け入れられます。 - スキーマのルートはオブジェクト型の値を受け入れる必要があります。関数呼び出し 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 | stdout への出力内容 |
|---|---|
text (デフォルト) | JSON.stringify(payload) + "\n" — 1 行、検証済みオブジェクト。 |
json | メッセージオブジェクトの単一 JSON 配列(完全なイベントログ)。最後の要素は type: "result" メッセージで、result(JSON.stringify(payload))と structured_result(生オブジェクト)の両方を持ちます。 |
stream-json | 各イベントが JSONL として 1 行ずつ出力されます。終端の 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 | パース時に拒否されます。合成ツールの「session ends now」メッセージには TUI ループにターミネーターがありません。 |
--json-schema + --input-format stream-json | パース時に拒否されます。シングルショットのターミナルコントラクトは長期間動作する stream-json 入力プロトコルと互換性がありません。 |
--json-schema + --acp / --experimental-acp | パース時に拒否されます。ACP は合成ツールのターミナルコントラクトを尊重しない独自のターンループを実行します。 |
--json-schema でプロンプトも stdin パイプもない場合 | パース時に拒否されます。ヘッドレスモードにはプロンプトが必要です — -p、位置引数、またはパイプで渡してください。 |
--bare + --json-schema | サポートされています。合成ツールは bare の 3 つのツール(read_file、edit、run_shell_command)と共に登録されます。 |
サブエージェント内での --json-schema | ツールは登録されません。トップレベルの実行のメイン/ドレインターンのみがターミナルコントラクトを尊重します。サブエージェントがツールを呼び出すと「session ends now」を受け取りますが、そのループにはターミネーターがないため実行が継続されてしまいます。 |
リトライと失敗モード
コストに関する注意.
--json-schema実行においてトークン消費を増加させる 2 つの要因があります。どちらも設計時に考慮する価値があります:
- スキーマがすべてのターンに埋め込まれる. スキーマは最初のリクエストだけでなく、すべてのモデルリクエストで
structured_output関数宣言のparametersブロックとして送信されます。大きなスキーマ(4 MiB のパース上限まで)は、実行全体でターンあたりの入力トークンを比例的に増加させます。- 各検証リトライは完全なモデルターンです. モデルが繰り返しスキーマを満たせない場合、失敗ごとに(リクエスト + 推論 + レスポンス)が乗算されます。モデルを誘導するのに十分なくらいには制約し、最初の試行で正確に呼び出せるくらいにはシンプルなスキーマを維持してください。リトライが予想される場合は
--max-session-turnsを増やしてください。
最初に有効な呼び出しが行われた時点でセッションは終了します。それまでは:
- 引数が検証に失敗する.
structured_outputは Ajv のメッセージを含むツール結果エラーを返し、モデルは次のターンでそれを確認し、引数を修正して再度呼び出す場合があります。 - モデルが
structured_outputと同じターンに副作用を持つツールを呼び出す. プリスキャンが兄弟ツールを抑制します — 構造化呼び出しが最終的に検証を通過するかどうかにかかわらず、実行されません。2 つのパスはモデルが次に見るものによって分岐します:- 検証成功: セッションは即座に終了し、モデルは別のターンを取得しません — 抑制された兄弟ツールはサイレントに破棄されます。
- 検証失敗: モデルは別のターンを取得し、抑制された呼び出しに対して合成された「Skipped:」
tool_resultを確認します。そのため、別のターン(structured_outputを含まないターン)でその呼び出しを再発行できます。
- モデルが
structured_outputを呼び出す代わりにプレーンテキストを出力する. 終了コード1。エラーメッセージにはターン数とモデルの出力の切り詰めたプレビューが含まれるため、実際に何を出力したかを確認できます。 - セッションが
maxSessionTurnsに達する. 終了コード53。標準の「Reached max session turns」終了に加え、ツールが呼び出されなかった、structured_outputがパーミッションルールによって拒否された、またはスキーマが満足不可能という 3 つの一般的な原因を指す--json-schema固有のヒントが表示されます。 - セッションが中断される(SIGINT / Ctrl-C). 終了コード
130。構造化結果は通常出力されませんが、シャットダウン待機ループはアボートシグナルをポーリングしないため、成功した呼び出しがキャプチャされた後、結果が stdout に到達する前に SIGINT が到着した場合は stdout に出力される可能性があります。真の情報源として終了コードを使用してください。
プライバシー
structured_output を通じて送信した引数は構造化ペイロードそのものです — すでに stdout に出力されています。同じペイロードが端末の外に持ち出される可能性のあるデバイス上のサーフェスに二重に保存されるのを避けるため、引数は以下の場所でプレースホルダー
{ __redacted: 'structured_output payload (see stdout result)' } に置き換えられます:
ToolCallEventテレメトリパス(OTLP エクスポート、QwenLogger、ui-telemetry ストリーム、chat-recording UI イベントミラー)。~/.qwen/projects/<sanitized-cwd>/chats/<sessionId>.jsonlのオンディスク chat-recording JSONL(--continue/--resumeでモデルコンテキストに再供給される)、検証失敗のすべてのリトライを含む。
ツール呼び出しのメトリクス(所要時間、成功、判断)と周辺のイベントメタデータは保持されます。
スキーマはモデルプロバイダーに送信されます. 難読化はローカルサーフェスの 呼び出し引数 のみに適用されます。スキーマ自体はすべてのモデルリクエストで
structured_output関数宣言のparametersブロックとして送信されます — そのため、スキーマ内に記述したリテラル値(enum、const、default、examples、description、$commentなど)はプロンプトテキストと同様にプロバイダーに平文で届きます。スキーマは形状と制約を記述するものとして扱い、プロバイダーに対してパブリックなものとして扱い、秘密情報、顧客レコード、その他の機密ペイロードをスキーマ本体に含めないようにしてください。
フックは生の引数を参照します. 上記の難読化はテレメトリと chat-recording にのみ適用されます。
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はツールとして存在せず、モデルは自由形式のテキストで応答します。- 再開された chat-recording 内の
__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の注意点. Bare モードは settings 由来の入力のほとんどを無視します。settings レベルのpermissions.denyとtools.excludeを含みます。合成ツールは登録されたままとなるため、settings のみによるstructured_outputの拒否は--bare下でサイレントに無効となります。argv レベルの--exclude-tools structured_outputは bare モードでも適用されます — bare 実行をロックダウンする必要がある場合は settings ではなくフラグを使用してください。
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