Claude Code CLI JSON structured output automation Python 2026

claude -p のJSON出力が本番環境で壊れる — 構造化出力で解決する

The Prompt Shelf ·

パイプラインを作った。claude -p "このファイルから設定値を取り出してJSONで返して" を実行する。テストでは動く。本番では、こんな出力が返ってくる:

以下が見つかった設定値です:

```json
{
  "database_url": "postgres://...",
  "api_key": "...",

なお、max_connections フィールドは2箇所に現れています…

{ “database_url”: “postgres://…”,


JSONパーサーが例外を投げる。パイプラインが落ちる。最初からやり方が間違っていたのかと思い始める。

間違っていた。実際に何が起きているのか、どう直すのかを説明する。

---

## なぜ claude -p の出力がJSONパーサーを壊すのか

`claude -p` フラグはClaude Codeを「プリント」モードで実行する — 非インタラクティブなセッションで、stdoutに出力してから終了する。スクリプティング用に設計されている。しかし、デフォルトの出力フォーマットは*人間が読めるテキスト*であり、機械が読むJSONではない。

「JSONを返して」と自然言語で頼むとき、言語モデルに対してレスポンスのフォーマットを求めている。大抵は従う。しかし、パーサーに届くまでにいくつかの問題が出力を壊す可能性がある:

### 問題1: 会話的な前置き

Claudeはデフォルトで会話型モデルだ。明示的な出力フォーマット指定がなければ、プロンプトを関数呼び出しではなく会話として扱う。つまり、求めたJSONが以下で囲まれる可能性がある:

- 導入文(「以下がリクエストされた値です:」)
- JSON後の説明コメント
- JSONブロックをトリプルバッククォートで囲んだインラインマークダウン

これらはすべて `json.loads()` を壊す。

### 問題2: 拡張思考のリーク

Claudeの拡張思考機能は、最終レスポンスの前に推論トークンを生成する。ほとんどの設定では、思考トークンは出力前にストリップされる。しかし、一部のストリーミング設定や古いCLIバージョンでは、思考出力がレスポンスと一緒にstdoutにリークすることがある。

リークした思考ブロックはこのような見た目になる:
ユーザーはデータベース設定値を含むJSONオブジェクトを求めている。抽出するには…

{ “database_url”: “postgres://…” }


パーサーは `<thinking>` を無効なJSONとして認識し、即座に失敗する。

### 問題3: ストリーミングのアーティファクト

Claude APIはレスポンスをトークン単位でストリーミングする。`claude -p` CLIはこのストリームをstdoutに書き込む前に再組み立てする — しかし再組み立てしているのは*テキスト*であり、構造化データではない。改行処理、バッファリング、ターミナルエンコーディングのすべてが、特定の環境で有効なJSONをパース不能にするアーティファクトを引き込む可能性がある。

これは非決定論的なので最悪のカテゴリだ。開発環境(自分のターミナル、自分のロケール、自分のバッファサイズ)では問題なく見えて、本番環境(別のOS、別のPythonバージョン、別のstdoutエンコーディング)では失敗する。

---

## 解決策: --output-format json

Claude CodeのCLIには `--output-format` フラグがあり、3つのオプションがある:

- `text` — 人間が読めるテキスト(デフォルト)
- `json` — 構造化JSONエンベロープ
- `stream-json` — ストリーミングコンシューマー向けの改行区切りJSON

スクリプティング用途には `--output-format json` が正解だ。レスポンス全体をJSONエンベロープで包む:

```bash
claude -p "データベース設定を抽出してJSONオブジェクトとして返して" \
  --output-format json

出力:

{
  "type": "result",
  "subtype": "success",
  "cost_usd": 0.0023,
  "duration_ms": 1847,
  "result": "{\n  \"database_url\": \"postgres://localhost:5432/prod\",\n  \"max_connections\": 100,\n  \"ssl_mode\": \"require\"\n}",
  "session_id": "sess_01abc..."
}

result フィールドにはモデルのレスポンスが文字列として含まれる。JSONを返すよう頼んだなら、その文字列にJSONが入っている。外側のエンベロープを先にパースしてから result をパースする。

これで会話的な前置き問題が完全に解決するわけではない — Claudeは result の中でJSONを説明テキストで包むかもしれない。それはプロンプトで対処する必要がある。


クリーンなJSONを返すプロンプトの書き方

--output-format json フラグが外側のエンベロープを担当する。内側のコンテンツはプロンプトが担当する。

確実にJSONを返すプロンプトとそうでないプロンプトの違いは、出力フォーマットをどれだけ具体的に指示するかだ:

信頼できない:

このファイルから設定値を取り出してJSONを返して。

信頼できる:

以下のファイルから設定値を取り出してください。

他のテキスト、説明、マークダウン書式を一切含まない、JSONオブジェクトのみを返してください。
JSONオブジェクトは以下の正確なキーを持つ必要があります: database_url, max_connections, ssl_mode。
値が見つからない場合は null を使用してください。

バッククォート、コードブロック、JSONオブジェクトの外側のテキストは一切含めないでください。

重要なフレーズ:

  • 「他のテキストを一切含まない、JSONオブジェクトのみを返してください」
  • 「バッククォート、コードブロック、JSONオブジェクトの外側のテキストは一切含めないでください」
  • 型とnullの挙動を含む明示的なフィールド名

冗長に聞こえる。実際そうだ。しかし、動くパイプラインと、再現が難しい方法で3%の確率で失敗するパイプラインの違いでもある。


バリデーションとリトライを含むPython実装

本番対応のラッパーで、パース、バリデーション、リトライを処理する:

import json
import subprocess
import time
from typing import Any, Optional
from pydantic import BaseModel, ValidationError


class ClaudeJSONResult(BaseModel):
    """外側の claude --output-format json エンベロープ用Pydanticモデル。"""
    type: str
    subtype: str
    cost_usd: float
    duration_ms: int
    result: str
    session_id: Optional[str] = None


def run_claude_json(
    prompt: str,
    max_retries: int = 3,
    retry_delay: float = 2.0,
    max_turns: int = 1,
) -> dict[str, Any]:
    """
    --output-format json で claude -p を実行し、パース済みの内側の結果を返す。
    
    Args:
        prompt: Claudeに送るプロンプト
        max_retries: パース失敗時のリトライ回数
        retry_delay: リトライ間の待機秒数
        max_turns: 最大会話ターン数(シングルショット用デフォルト1)
    
    Returns:
        ClaudeのレスポンスからパースされたJSONのdict
    
    Raises:
        ValueError: 全リトライ後も出力をパースできない場合
        RuntimeError: claude CLIがゼロ以外の終了コードを返した場合
    """
    cmd = [
        "claude",
        "-p", prompt,
        "--output-format", "json",
        "--max-turns", str(max_turns),
    ]
    
    last_error: Optional[Exception] = None
    
    for attempt in range(max_retries):
        if attempt > 0:
            time.sleep(retry_delay)
        
        try:
            proc = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                encoding="utf-8",
            )
            
            if proc.returncode != 0:
                raise RuntimeError(
                    f"claude が終了コード {proc.returncode} で終了: {proc.stderr}"
                )
            
            # 外側のJSONエンベロープをパース
            outer = ClaudeJSONResult.model_validate_json(proc.stdout)
            
            if outer.subtype != "success":
                raise ValueError(f"Claudeが非successのsubtypeを返した: {outer.subtype}")
            
            # 内側の結果から偶発的なマークダウンフェンスを除去
            inner_text = strip_markdown_fences(outer.result.strip())
            
            # 内側のJSON(Claudeが実際に返したもの)をパース
            return json.loads(inner_text)
        
        except (json.JSONDecodeError, ValidationError) as e:
            last_error = e
            continue
    
    raise ValueError(
        f"{max_retries}回の試行後にClaude JSONの出力をパースできなかった。"
        f"最後のエラー: {last_error}"
    )


def strip_markdown_fences(text: str) -> str:
    """
    ClaudeがJSONをマークダウンコードフェンスで囲んでいた場合に除去する。
    ```json ... ``` と ``` ... ``` のパターンに対応。
    """
    lines = text.splitlines()
    
    if not lines:
        return text
    
    # 開始フェンスを除去
    first_line = lines[0].strip()
    if first_line.startswith("```"):
        lines = lines[1:]
    
    # 終了フェンスを除去
    if lines and lines[-1].strip() == "```":
        lines = lines[:-1]
    
    return "\n".join(lines)

使用例:

prompt = """
以下の設定ファイルの内容からデータベース設定を取り出してください。

他のテキスト、説明、マークダウンを一切含まない、JSONオブジェクトのみを返してください。
使用するキー: database_url, max_connections, ssl_mode, port。
値が見つからない場合は null を使用してください。

設定ファイル:
---
DATABASE_URL=postgres://user:pass@localhost:5432/prod
MAX_CONNECTIONS=100
SSL_MODE=require
---
"""

try:
    config = run_claude_json(prompt)
    print(f"Database URL: {config['database_url']}")
    print(f"Max connections: {config['max_connections']}")
except ValueError as e:
    print(f"パース失敗: {e}")
except RuntimeError as e:
    print(f"Claude CLI エラー: {e}")

スキーマバリデーションの追加

上記のリトライロジックはJSONパースエラーをキャッチするが、セマンティックエラーはキャッチしない — Claudeは期待するスキーマに一致しない有効なJSONを返すかもしれない。内側の結果にもPydanticバリデーションを追加する:

from pydantic import BaseModel, field_validator
from typing import Optional


class DatabaseConfig(BaseModel):
    database_url: str
    max_connections: int
    ssl_mode: str
    port: Optional[int] = None
    
    @field_validator("max_connections")
    @classmethod
    def max_connections_positive(cls, v: int) -> int:
        if v <= 0:
            raise ValueError("max_connections は正の値である必要がある")
        return v
    
    @field_validator("ssl_mode")
    @classmethod
    def ssl_mode_valid(cls, v: str) -> str:
        valid_modes = {"disable", "allow", "prefer", "require", "verify-ca", "verify-full"}
        if v not in valid_modes:
            raise ValueError(f"ssl_mode は {valid_modes} のいずれかである必要がある")
        return v


def extract_database_config(config_text: str) -> DatabaseConfig:
    prompt = f"""
以下のテキストからデータベース設定を取り出してください。

他のテキスト、説明、マークダウンを一切含まない、JSONオブジェクトのみを返してください。
必須キー: database_url (文字列), max_connections (整数), ssl_mode (文字列)。
オプションキー: port (整数またはnull)。

テキスト:
---
{config_text}
---
"""
    
    raw = run_claude_json(prompt)
    return DatabaseConfig.model_validate(raw)

Claudeが有効なセット外の ssl_mode を返した場合、Pydanticは即座に ValidationError を発生させる。run_claude_json のリトライループがそれをキャッチしてリトライする。リトライ時、Claudeはサンプリングがわずかに異なるため自己修正することが多い。

このパターン — パース失敗だけでなくバリデーション失敗でもリトライ — が、デモレベルの統合と本番レベルの統合を分ける。


stream-json フォーマットの処理

長時間実行するClaudeタスクで出力を段階的に処理する必要がある場合、--output-format stream-json は改行区切りのJSONを出力する。各行がストリーミングイベントを表すJSONオブジェクトだ。

import json
import subprocess
from typing import Generator


def stream_claude_events(prompt: str) -> Generator[dict, None, None]:
    """claude --output-format stream-json からパースされたイベントをyieldする。"""
    cmd = [
        "claude",
        "-p", prompt,
        "--output-format", "stream-json",
        "--max-turns", "1",
    ]
    
    proc = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        encoding="utf-8",
    )
    
    for line in proc.stdout:
        line = line.strip()
        if not line:
            continue
        try:
            yield json.loads(line)
        except json.JSONDecodeError:
            # 部分的な行や非JSON出力 — スキップ
            continue
    
    proc.wait()
    if proc.returncode != 0:
        stderr = proc.stderr.read()
        raise RuntimeError(f"claude が終了コード {proc.returncode} で終了: {stderr}")


def extract_result_from_stream(prompt: str) -> str:
    """ストリーミングモードでclaudeを実行し、最終結果テキストを返す。"""
    result_text = ""
    
    for event in stream_claude_events(prompt):
        event_type = event.get("type")
        
        if event_type == "assistant":
            # assistantメッセージのコンテンツからテキストを取り出す
            for block in event.get("message", {}).get("content", []):
                if block.get("type") == "text":
                    result_text += block.get("text", "")
        
        elif event_type == "result":
            # 最終resultイベント — 利用可能ならresultフィールドを使う
            if event.get("subtype") == "success":
                result_text = event.get("result", result_text)
            break
    
    return result_text

stream-jsonフォーマットが有用な場面:

  • Claudeがマルチターン作業をしていて進捗更新が欲しい場合
  • タスクが長時間実行されていてストールを検知したい場合
  • Claudeの進捗をリアルタイムで表示するUIを作っている場合

シンプルな抽出タスクには --output-format json の方がすっきりする。実際にストリーミング動作が必要な場合にstream-jsonを使う。


環境とバージョンの考慮事項

「ローカルでは動くのにCIで壊れる」という失敗を引き起こす、JSON出力に特有のいくつかの事項がある:

Pythonのエンコーディング。 一部のLinuxシステムでは、subprocessのテキスト出力のデフォルトエンコーディングはUTF-8ではなくASCIIだ。Claudeのレスポンスに非ASCII文字が含まれていると(分析しているコンテンツ内に、JSONキーだけでなく)、UnicodeDecodeError が発生する。subprocess.run()encoding="utf-8" を明示的に渡すことで修正できる。

Claude Codeのバージョン固定。 JSONエンベロープのフォーマットはClaude Codeのバージョン間で変わってきた。上記の例にある typesubtyperesult フィールド構造は2026年中頃時点で最新だ。古いバージョンを使っている場合、フィールド名が異なる可能性がある。CIでは npm install -g @anthropic-ai/claude-code@X.Y.Z でClaude Codeのバージョンを固定する。

ANTHROPIC_API_KEYのスコープ。 claude -p コマンドは環境変数の ANTHROPIC_API_KEY を使用する。CIではこれを明示的に設定する必要がある。ローカル開発では、Claude Codeは通常インタラクティブなセットアップ時に保存している。失敗パターンは、期待されるエンベロープの代わりに非JSONエラーメッセージをstdoutに出力するサイレントな認証エラーで、パーサーを混乱させる形で壊す。


APIを直接使う場合は?

構造化出力を確実に必要とする本番ワークロードでは、CLIをシェルで呼び出すのではなく、response_format パラメータを使ってAnthropicのAPIを直接呼び出すことを検討する。

APIの構造化出力モードはJSONスキーマに準拠したレスポンスを保証する:

import anthropic
import json

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": "データベース設定を取り出してください。キー: database_url, max_connections, ssl_mode のJSONを返してください。"
    }]
)

# プロンプトに従いレスポンステキストはクリーンなJSONのはず
config = json.loads(response.content[0].text)

CLIアプローチ(claude -p)が適切な場面:

  • Claude CodeのファイルシステムコンテキストがClaude Codeのファイルシステムコンテキストが必要なスクリプト(プロジェクト内のファイルを読む)
  • Claude CodeのツールUse機能(Bash、Edit、Read)を使うワークフロー
  • エンジニアが使っているものと同じClaude Codeセッションが欲しいタスク

APIアプローチが適切な場面:

  • ファイルシステムアクセスが不要な純粋なテキスト処理
  • シェルプロセスのオーバーヘッドが問題になる大量処理パイプライン
  • レスポンス構造の保証が必要な本番サービス

ほとんどの自動化タスクには、--output-format json を使ったCLIが正しい出発点だ。出力フォーマットと繰り返し格闘していることに気づいたら、それはAPIに切り替えるサインだ。

Claude Codeのパーミッションとこれが利用可能なツールについての全体像は Claude Code Best Practices: The Official and Community-Tested Guide for 2026 を参照。


クイックリファレンス

# 基本的なJSON出力
claude -p "your prompt" --output-format json

# ターン数制限付き(シンプルな抽出でマルチターンを防ぐ)
claude -p "your prompt" --output-format json --max-turns 1

# 長時間タスク用ストリーミングJSON
claude -p "your prompt" --output-format stream-json

# Pythonでの結果パース(シンプルなケースのワンライナー)
python3 -c "
import json, subprocess
out = subprocess.run(['claude', '-p', 'return {\"ok\": true}', '--output-format', 'json'], capture_output=True, text=True)
envelope = json.loads(out.stdout)
result = json.loads(envelope['result'])
print(result)
"

本番で機能するパターン:

  1. 外側のエンベロープに --output-format json
  2. 内側のJSONフォーマットに明示的なプロンプト指示
  3. 安全網として strip_markdown_fences()
  4. パース済み結果へのPydanticバリデーション
  5. json.JSONDecodeErrorValidationError 両方でリトライ

Claudeのクリーンな出力は、2層の問題として扱えば信頼できる: CLIの出力フォーマット(フラグで修正)とモデルのレスポンスフォーマット(プロンプトで修正しスキーマでバリデーション)。両方の層を処理すれば失敗は消える。

Related Articles

Claude Code × GitHub Actions:パーミッションエラーを解決する3つの実証済みパターン(2026年版)

GitHub ActionsでClaude Codeを動かしてパーミッションエラーに悩んでいる人向け。エラーが起きる理由と解決策を、allowlist設定・bypassPermissionsモード・公式claude-code-actionという3つの実証済みパターンで解説する。

Claude Code ヘッドレスモードで PR レビューを自動化する — レビュアーが Claude をインストールできなくても大丈夫

claude -p を使って GitHub Actions で Claude Code の PR レビューを実行する方法を解説。ローカルに Claude を入れられないチームでも、diff の渡し方・コメント自動投稿・セキュリティ/パフォーマンス向けプロンプト・max-turns によるコスト管理まで網羅。

Claude Code Hooks: プリ編集フックがすべてを止める理由と対処法

Claude Code のフックはゼロ以外の終了コードで操作全体をブロックする。各フック種別の挙動の違い、そしてワークフローをフリーズさせずにグレースフルに失敗するフックの設計方法を解説する。

Python プロジェクト向け CLAUDE.md: 完全テンプレート+ルール集(2026年版)

Pythonプロジェクト向けの本番対応CLAUDE.mdテンプレート。モダンなPythonツールチェーン(uv、ruff、pyproject.toml、pytest)対応。FastAPI・Django・データサイエンス別パターン付き。コピペして使える。

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules