# Claude Structured Output 사용법: output_config.format과 400 오류 실측

> Claude에서 스키마에 맞는 JSON을 받으려면 output_config.format을 넣고 text 블록을 파싱합니다. 거부·max_tokens 잘림·강제되지 않는 제약은 코드로 검증합니다.

- URL: https://blog.laozhang.ai/ko/posts/claude-structured-output
- Published: 2026-10-30
- Updated: 2026-10-30
- Author: LaoZhang AI Team (https://blog.laozhang.ai/ko/about)
- Topic: API 가이드
- Tags: Claude API, Structured Outputs, JSON Schema, output_config, strict tool use, Python

---
Claude에도 구조화된 출력(Structured Outputs)이 있고, 2026년 10월 30일 기준 Claude API에서 정식(GA) 기능입니다. 응답 전체를 JSON으로 받으려면 Messages API 요청에 `output_config.format`을 넣어 `type: "json_schema"`와 스키마를 지정하고, 응답 `content`의 `text` 블록을 파싱합니다. 도구 호출 인자를 스키마에 맞추고 싶다면 도구 정의에 `strict: true`를 붙입니다.

2025년 베타 시절의 최상위 `output_format` 필드와 `anthropic-beta: structured-outputs-2025-11-13` 헤더는 지원 중단된 방식이므로 새 코드에는 쓰지 않습니다. Anthropic 문서는 베타 헤더 없이 이 필드를 보내면 400이 난다고 설명하지만, 2026년 10월 30일 LaoZhang API를 거쳐 보낸 요청은 헤더가 있든 없든 200을 돌려주면서 스키마를 조용히 버리고 일반 텍스트로 답했습니다. 오류가 나지 않는다고 형식이 적용됐다고 볼 수 없는 이유입니다.

*안내: [laozhang.ai는 이 블로그 운영팀의 자사 서비스입니다](https://blog.laozhang.ai/ko/about#laozhang). 그 가격은 독립적인 비교가 아니라 자사 제시 가격입니다.*

스키마가 적용돼도 "항상 유효한 JSON"에는 조건이 붙습니다. `stop_reason`이 `refusal`이나 `max_tokens`이면 JSON이 스키마와 다르거나 중간에 끊길 수 있고, `minLength` 같은 일부 제약은 받아들여진 뒤에도 지켜지지 않았습니다. 그래서 `stop_reason` 확인과 Pydantic·Zod 같은 로컬 검증을 파싱 코드에 함께 둡니다.

## output_config.format으로 JSON 받기: cURL 요청과 응답 위치

가장 짧은 요청은 Messages API 본문에 `output_config.format`을 하나 추가하는 것입니다. 객체마다 `additionalProperties: false`를 명시하고, 받아야 할 필드는 `required`에 모두 넣습니다. 아래는 [Anthropic Claude 구조화된 출력 문서](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)의 연락처 추출 예시와 같은 스키마로, Anthropic API에 직접 보낼 때는 `x-api-key` 헤더를 씁니다.

```bash
curl https://api.anthropic.com/v1/messages \
  -H "content-type: application/json" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -d '{
    "model": "claude-haiku-5-5",
    "max_tokens": 1024,
    "messages": [{
      "role": "user",
      "content": "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm."
    }],
    "output_config": {
      "format": {
        "type": "json_schema",
        "schema": {
          "type": "object",
          "properties": {
            "name": {"type": "string"},
            "email": {"type": "string"},
            "plan_interest": {"type": "string"},
            "demo_requested": {"type": "boolean"}
          },
          "required": ["name", "email", "plan_interest", "demo_requested"],
          "additionalProperties": false
        }
      }
    }
  }'
```

JSON은 별도 필드가 아니라 `content[0].text`에 문자열로 들어옵니다. 2026년 10월 30일 같은 요청을 LaoZhang API(laozhang.ai)의 `https://api.laozhang.ai/v1/messages`로 `claude-haiku-5-5`에 보냈을 때 응답은 다음과 같았고, `stop_reason`은 `end_turn`, 입력 333토큰, 출력 46토큰, 3.61초였습니다.

*안내: [laozhang.ai는 이 블로그 운영팀의 자사 서비스입니다](https://blog.laozhang.ai/ko/about#laozhang). 그 가격은 독립적인 비교가 아니라 자사 제시 가격입니다.*

```json
{
  "content": [
    {
      "type": "text",
      "text": "{\"demo_requested\": true, \"email\": \"john@example.com\", \"name\": \"John Smith\", \"plan_interest\": \"Enterprise\"}"
    }
  ],
  "stop_reason": "end_turn"
}
```

아래 실측은 모두 같은 날 같은 게이트웨이를 거친 결과입니다. 모델은 `claude-haiku-5-5`(thinking 확인 한 건만 `claude-sonnet-5-5`)였고, 응답의 `model` 필드는 매번 요청한 ID와 같았습니다. api.anthropic.com에 직접 보낸 결과는 포함되지 않았으므로, 아래에서 문서와 다르게 나온 동작은 Anthropic 직접 API에서도 같다고 볼 수 없습니다. 게이트웨이는 Anthropic 형식 엔드포인트라서 위 cURL에서 주소와 키만 바꾸면 됩니다.

## Python messages.parse와 TypeScript zodOutputFormat 예제

SDK를 쓴다면 스키마를 직접 JSON으로 쓰기보다 Python은 Pydantic 모델, TypeScript는 Zod 스키마를 넘기는 편이 낫습니다. Python의 `client.messages.parse()`는 모델을 스키마로 바꿔 `output_config.format`으로 보내고, 응답을 검증한 뒤 `parsed_output`에 객체를 돌려줍니다. 여기서 `output_format=`은 SDK 헬퍼의 인자 이름일 뿐이고, 지원 중단된 것은 요청 본문 최상위의 `output_format` 필드입니다.

```python
from typing import Literal, Optional
import anthropic
from pydantic import BaseModel, Field

class Lead(BaseModel):
    name: str = Field(min_length=2)
    email: str
    plan: Literal["Starter", "Team", "Enterprise"]
    seats: int = Field(ge=1, le=500)
    phone: Optional[str] = None

client = anthropic.Anthropic()  # 게이트웨이를 쓰면 base_url="https://api.laozhang.ai"

resp = client.messages.parse(
    model="claude-haiku-5-5",
    max_tokens=512,
    messages=[{"role": "user",
               "content": "John Smith (john@example.com) wants the Enterprise plan for 40 seats."}],
    output_format=Lead,
)
print(resp.parsed_output)
```

2026년 10월 30일 anthropic 2.3.0, pydantic 2.14 환경에서 이 코드를 게이트웨이로 실행하자 `parsed_output`은 `name='John Smith'`, `plan='Enterprise'`, `seats=40`, `phone=None`이었고 입력은 489토큰이었습니다. `min_length`와 `ge`/`le`는 API가 지원하지 않는 제약이라 SDK가 따로 처리하는데, 그 동작은 다음 절의 스키마 표 아래에서 설명합니다.

TypeScript는 `zodOutputFormat()`으로 감싼 스키마를 `output_config.format`에 넣고 `client.messages.parse()`를 호출합니다. Anthropic 문서 기준으로 `parse()`가 응답을 검증해 스키마 타입의 `parsed_output`을 돌려줍니다. 아래 코드는 문서 예시를 줄인 것입니다.

```typescript
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";

const Contact = z.object({
  name: z.string(),
  email: z.string(),
  plan_interest: z.string(),
  demo_requested: z.boolean(),
});

const client = new Anthropic();
const response = await client.messages.parse({
  model: "claude-haiku-5-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "John Smith (john@example.com) wants an Enterprise demo next Tuesday." }],
  output_config: { format: zodOutputFormat(Contact) },
});
console.log(response.parsed_output);
```

Zod 없이 순수 JSON 스키마를 넘기는 `jsonSchemaOutputFormat()`도 있지만, 문서에 따르면 이 헬퍼는 스키마만 변환하고 응답은 검증하지 않습니다. 길이나 범위 제약이 있는 스키마라면 `zodOutputFormat()`을 쓰거나 직접 검증 코드를 붙여야 합니다.

## JSON 출력 vs strict 도구: 최종 답이냐 도구 인자냐

Claude가 사용자나 다음 시스템에 넘길 최종 답 자체가 객체여야 하면 JSON 출력(`output_config.format`)을, Claude가 내 함수를 호출할 때 넘기는 인자가 틀리면 안 되면 strict 도구(`strict: true`)를 씁니다. 에이전트처럼 도구를 여러 번 부른 뒤 정해진 형식으로 결과를 내야 한다면 두 기능을 한 요청에 함께 넣을 수 있습니다. 두 기능은 같은 JSON Schema 제한을 공유하므로 다음 절의 표가 양쪽에 똑같이 적용됩니다.

| 기준 | JSON 출력 | strict 도구 |
| --- | --- | --- |
| 설정 위치 | 요청 최상위 `output_config.format` | 도구 정의의 `strict: true` + `input_schema` |
| 스키마가 묶는 것 | Claude의 최종 응답 텍스트 | 도구 이름과 도구 입력(`input`) |
| 결과가 오는 곳 | `text` 블록의 JSON 문자열 | `tool_use` 블록의 `input` 객체 |
| 정상 종료 `stop_reason` | `end_turn` | `tool_use` |
| 맞는 작업 | 정보 추출, 분류, 보고서, 에이전트 최종 출력 | 예약, 결제, DB 쓰기처럼 인자 오류가 사고로 이어지는 호출 |
| 2026-10-30 게이트웨이 실측 | 4개 필드 JSON, 입력 333토큰 | 아래 예약 도구, 입력 682토큰 |

strict 도구의 효과는 타입이 흔들리지 않는다는 점입니다. Anthropic의 [strict tool use 문서](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use)는 정수여야 할 `passengers`가 strict 없이는 `"two"`나 `"2"`로 올 수 있고 strict를 켜면 항상 `2`로 온다고 설명합니다. 2026년 10월 30일 `plan`과 `weekday`를 enum으로, `hour_24`를 정수로, `email`을 `format: "email"`로 둔 예약 도구를 `tool_choice`로 강제해 호출하자 다음 입력이 돌아왔습니다.

```json
{
  "type": "tool_use",
  "name": "create_demo_booking",
  "input": {"email": "john@example.com", "hour_24": 14, "plan": "Enterprise", "weekday": "Tue"}
}
```

"오후 2시"가 정수 `14`로, "다음 화요일"이 enum 값 `"Tue"`로 정리됐습니다. 다만 한 번의 결과이고, strict는 형식만 보장할 뿐 "다음 화요일"이 실제로 어느 날짜인지는 앱이 계산해야 합니다. 도구가 많을 때는 사고 위험이 큰 도구에만 `strict`를 붙입니다. 요청 하나에 strict 도구는 20개까지라는 한도가 있기 때문입니다.

## 400이 나는 스키마와 받아들여져도 강제되지 않는 제약

JSON Schema의 기본 타입, `enum`, `const`, `anyOf`, 내부 `$ref`, `required`, 일부 문자열 `format`은 지원되고, 수치·길이 제약과 재귀 스키마는 지원되지 않습니다. 문제는 지원되지 않는 키워드가 모두 400으로 끝나지는 않는다는 점입니다. 2026년 10월 30일 게이트웨이에서는 `minimum`과 재귀 `$ref`가 400을 냈지만, `minLength`·`maxLength`는 200으로 받아들여진 뒤 지켜지지 않았습니다.

| 스키마 키워드 | Anthropic 문서 | 2026-10-30 게이트웨이 결과 | 코드에서 할 일 |
| --- | --- | --- | --- |
| `additionalProperties` 생략 | 객체마다 `false` 필수 | 200, 4개 필드 그대로 출력 | 그래도 항상 `false`로 명시 |
| `minimum`, `maximum`, `multipleOf` | 미지원, 400 | 400, `property 'minimum' is not supported` | 스키마에서 빼고 범위는 로컬 검증 |
| 재귀 `$ref` (트리 구조) | 미지원 | 400, `Circular reference detected` | 깊이를 정해 비재귀 구조로 펼침 |
| `minLength`, `maxLength` | 미지원, 400 | 200, 12자 규칙에 `"ABC"` 반환 | 길이는 반드시 로컬 검증 |
| 문자열 `enum`, `const` | 지원, 대소문자는 보장 안 됨 | 5회 모두 스키마 표기 그대로 | 대소문자 무시 비교 |
| 선택 필드 (`required`에 없음) | 선택 필드로 취급 | 생략 대신 `null`로 반환 | "없음 또는 null" 둘 다 처리 |
| `minItems` 2 이상, 역참조·전후방 탐색 정규식 | 미지원 | 실측 없음 | 로컬 검증 |

400일 때 돌아온 메시지 원문은 다음과 같습니다. 오류 유형은 둘 다 `invalid_request_error`였고, 메시지 앞의 `output_config.format.schema`가 어느 스키마에서 걸렸는지 알려 줍니다.

```text
output_config.format.schema: For 'integer' type, property 'minimum' is not supported

output_config.format.schema: Invalid schema: Circular reference detected in schema definitions: node -> node. Self-referencing or mutually-referencing definitions are not supported.
```

`minLength`가 받아들여진 결과는 문서와 다르며, 직접 API에서는 문서대로 400이 날 수 있습니다. 어느 쪽이든 결론은 같습니다. 길이와 범위 같은 제약은 디코딩 단계에서 강제되지 않으므로 응답을 받은 뒤 코드가 확인해야 합니다.

### SDK는 지원 안 되는 제약을 description으로 옮깁니다

Python SDK의 `transform_schema()`를 위 `Lead` 모델에 실행하면 `name`의 `min_length=2`는 description `"{minLength: 2}"`로, `seats`의 `ge=1, le=500`은 `"{maximum: 500, minimum: 1}"`로 바뀌고, 모든 객체에 `additionalProperties: false`가 붙으며 `Optional[str] = None`은 `anyOf [string, null]`이 됩니다. Claude는 제약을 설명문으로만 읽기 때문에 지킬 수도, 어길 수도 있습니다.

실제로 2026년 10월 30일 "현재 좌석이 0개"라는 문장을 그대로 보고하게 하자 API는 `seats: 0`을 돌려줬고, `messages.parse()`가 응답을 원래 모델로 검증하면서 pydantic `ValidationError`(`Input should be greater than or equal to 1`)를 냈습니다. 범위를 벗어난 값이 프로그램 안으로 들어오지 않은 것은 SDK의 로컬 검증 덕분이고, 이 예외를 잡아 재요청하거나 사람에게 넘기는 처리는 직접 작성해야 합니다. Java SDK는 반대로 지원되지 않는 제약이 있으면 요청 전에 로컬에서 거부합니다.

![minimum·재귀 $ref는 400으로 거부되고, minLength·maxLength는 200으로 통과한 뒤 지켜지지 않으며, Python SDK는 ge·le 제약을 description으로 옮긴 뒤 seats 0 응답을 ValidationError로 막는 세 갈래 비교](https://blog.laozhang.ai/posts/ko/claude-structured-output/img/schema-keyword-lanes.webp)

스키마가 커지면 복잡도 한도도 봐야 합니다. 요청 하나의 모든 strict 스키마를 합쳐 선택 파라미터 24개, `anyOf`나 `["string", "null"]` 같은 유니언 타입 파라미터 16개까지이고, 이를 넘지 않아도 문법이 너무 커지면 `Schema is too complex for compilation` 400이 납니다. 필드를 가능한 한 `required`로 바꾸고, 깊은 중첩을 펼치고, 그래도 크면 요청을 나눕니다.

## JSON 대신 텍스트가 올 때: output_format 무시와 Extra inputs

200 응답인데 `text`가 `Here's the key information…` 같은 문장으로 시작한다면, 가장 먼저 의심할 것은 형식 지정이 Claude까지 전달되지 않았을 가능성입니다. 2026년 10월 30일 게이트웨이에 구 필드 `output_format`을 보낸 두 요청(베타 헤더 없음, `anthropic-beta: structured-outputs-2025-11-13` 있음)은 모두 200과 마크다운 목록으로 답했고, 입력 토큰은 스키마를 아예 넣지 않은 요청과 같은 64였습니다. 형식이 적용된 요청은 333토큰이었으니, 스키마와 형식 설명이 통째로 빠진 것입니다.

반대로 경로가 요청 본문을 엄격히 검사하면 오류가 납니다. LaoZhang API의 [Claude Code AWS 400 ValidationException 안내 문서](https://docs.laozhang.ai/faq/claude-code-aws-400-validation-error.md)는 AWS Claude 공식 채널이나 Bedrock 계열 채널이 받지 않는 필드에 `400 ValidationException` 또는 `Extra inputs are not permitted`를 돌려줄 수 있다고 설명합니다. `output_config.effort: Extra inputs are not permitted`처럼 필드 이름이 함께 나온다면 그 경로나 모델이 해당 필드를 받지 않는 경우이고, 메시지별 해결은 [AWS Bedrock Claude 400 오류: 메시지별 원인과 해결](https://blog.laozhang.ai/ko/posts/aws-claude-400-error)에 정리돼 있습니다.

*안내: [laozhang.ai는 이 블로그 운영팀의 자사 서비스입니다](https://blog.laozhang.ai/ko/about#laozhang). 그 가격은 독립적인 비교가 아니라 자사 제시 가격입니다.*

일반 텍스트가 오면 다음 순서로 확인합니다.

- [ ] 요청 본문에 `output_format`이 아니라 `output_config.format`이 있고, 그 안에 `"type": "json_schema"`가 있는가
- [ ] 같은 프롬프트를 스키마 없이 보냈을 때보다 `usage.input_tokens`가 늘었는가 (같다면 스키마가 버려진 것)
- [ ] `stop_reason`이 `end_turn`인가, 아니면 `refusal`·`max_tokens`인가
- [ ] Python SDK v1.0 이상에서 `client.beta.messages.create(output_format={...})`를 쓰고 있지 않은가 (`TypeError`가 남)
- [ ] 프록시나 게이트웨이가 `output_config`를 그대로 전달하는가 (`Extra inputs are not permitted`가 나면 전달 또는 수용 실패)
- [ ] 모델이 지원 목록에 있는가 (Claude 3.x 계열은 목록에 없음)

토큰 비교는 추가 비용이 거의 들지 않는 확실한 신호입니다. 프로덕션에서는 `json.loads()` 실패를 "모델이 실수했다"로 보고 재시도만 반복하기 전에, 입력 토큰이 늘었는지부터 로그로 남겨 두면 원인이 경로인지 모델인지 바로 갈립니다.

## 거부·max_tokens 잘림·키 순서: 파싱 전에 확인할 것

구조화된 출력은 대부분의 경우 스키마를 지키지만, Anthropic 문서도 예외를 명시합니다. 안전상 거부하면 HTTP 200에 `stop_reason: "refusal"`이 오고 토큰도 과금되며, 거부 메시지가 스키마보다 우선하므로 출력이 스키마와 맞지 않을 수 있습니다. `max_tokens`에 걸리면 `stop_reason: "max_tokens"`와 함께 JSON이 중간에 끊깁니다. 2026년 10월 30일 `max_tokens: 30`으로 아이디어 15개를 요청하자 200과 함께 `{"ideas": ["REST vs GraphQL vs gRPC: How to Choose the`에서 끝난 문자열이 왔습니다.

이 두 경우를 걸러 내고 로컬 검증까지 하는 최소 코드는 다음과 같습니다. thinking을 켜면 `content` 앞쪽에 다른 블록이 올 수 있으므로 `text` 블록을 타입으로 찾습니다.

```python
import json

def read_structured(resp, model_cls):
    if resp.stop_reason == "refusal":
        raise RuntimeError("refusal: 출력이 스키마와 다를 수 있어 파싱하지 않음")
    if resp.stop_reason == "max_tokens":
        raise RuntimeError("max_tokens: JSON이 잘렸을 수 있음, max_tokens를 올려 재시도")
    text = next(b.text for b in resp.content if b.type == "text")
    data = json.loads(text)                 # 형식이 버려졌다면 여기서 실패
    return model_cls.model_validate(data)   # 길이·범위 제약은 여기서 검증

PLANS = {"starter": "Starter", "team": "Team", "enterprise": "Enterprise"}
def normalize_plan(value: str) -> str:
    return PLANS[value.lower()]             # enum 대소문자 차이 흡수
```

![stop_reason이 refusal이면 중단, max_tokens면 재시도, json.loads가 실패하면 input_tokens 64 대 333으로 형식 무시 여부를 확인하고, model_validate 결과에 따라 재요청하거나 객체를 쓰는 파싱 전 확인 순서](https://blog.laozhang.ai/posts/ko/claude-structured-output/img/pre-parse-check-flow.webp)

거부를 줄이는 방법도 있습니다. 스키마에 "단계별 추론"을 담는 필드를 두면 `reasoning_extraction` 거부가 날 수 있으므로, 이유가 필요하면 짧은 설명 필드로 바꾸고 긴 추론은 thinking에 맡깁니다. `max_tokens`는 배열 길이가 정해지지 않은 스키마일수록 넉넉하게 잡습니다.

나머지 세 가지는 오류가 아니라 파싱 코드의 가정을 깨는 차이입니다. 문서는 필수 필드가 스키마 순서대로 먼저, 선택 필드가 그 뒤에 온다고 설명하지만, 2026년 10월 30일 게이트웨이에서는 모든 JSON 출력의 키가 알파벳 순(`demo_requested, email, name, plan_interest`)이었으므로 키 순서에 기대는 코드는 쓰지 않습니다. `required`에 없고 nullable도 아닌 `phone`은 생략되지 않고 `"phone": null`로 왔으니, 선택 필드는 "없음 또는 null"로 처리합니다. 문서가 경고하는 enum 대소문자 차이(`"Conversation topic 3"` 대신 `"Conversation Topic 3"`)는 5번 요청에서 한 번도 나오지 않았지만, 재현이 안 됐다고 보장되는 것은 아니므로 위 코드처럼 소문자로 맞춰 비교합니다.

예전처럼 assistant 메시지를 `{`로 미리 채워 JSON을 유도하는 방법도 쓸 수 없습니다. 문서상 prefill은 JSON 출력과 함께 쓸 수 없고, 같은 날 `claude-haiku-5-5`에 prefill을 보내자 `This model does not support assistant message prefill. The conversation must end with a user message.` 400이 났습니다. 이 오류는 형식 지정과 관계없이 모델 단위로 나는 것입니다.

## 형식 프롬프트 비용: 요청당 입력 269토큰 증가

구조화된 출력을 켜면 Claude가 형식 설명을 담은 시스템 프롬프트를 추가로 받기 때문에 입력 토큰이 늘고, 그만큼 과금됩니다. 2026년 10월 30일 같은 이메일 추출 요청에서 4개 필드 스키마를 넣으면 입력이 333토큰, 빼면 64토큰이었으므로 스키마와 형식 설명이 요청당 269토큰을 더했습니다. 스키마가 커지면 이 수치도 커집니다.

[Anthropic 가격 페이지](https://platform.claude.com/docs/en/about-claude/pricing)의 2026년 10월 30일 기준 입력 단가를 곱하면, 269토큰 × 100만 건 = 2억 6,900만 토큰(269 MTok)의 추가 비용은 다음과 같습니다. Haiku 5.5 단가는 프롬프트가 10만 토큰 이하일 때 기준입니다.

| 모델 | 입력 단가 (1M 토큰당) | 요청 100만 건당 추가 비용 |
| --- | --- | --- |
| Claude Haiku 5.5 | $0.10 | $26.90 |
| Claude Sonnet 5.5 | $2 | $538 |
| Claude Opus 5.5 | $4 | $1,076 |

출력은 오히려 줄 수 있습니다. 같은 예에서 일반 텍스트 답은 91토큰, JSON 답은 46토큰이었고, Sonnet 5.5 출력 단가 $10로 계산하면 45토큰 × $10 ÷ 100만 = 호출당 $0.00045가 줄어 입력 증가분 269토큰 × $2 ÷ 100만 = $0.000538과 거의 상쇄됩니다. 프롬프트 하나의 결과라서 일반화할 수는 없지만, 대량 처리라면 [Batch API의 50% 할인](https://platform.claude.com/docs/en/build-with-claude/batch-processing)이 구조화된 출력에도 적용되는 점이 더 큰 변수입니다. 모델별 단가 비교는 [Claude API와 OpenAI API 가격 비교: 워크로드별 계산](https://blog.laozhang.ai/ko/posts/claude-api-vs-openai-api-pricing)에서 볼 수 있습니다.

두 가지는 비용 계획에 함께 넣어 둡니다. `output_config.format`을 바꾸면 그 대화의 프롬프트 캐시가 무효화되므로 같은 대화 안에서 스키마를 자주 바꾸지 않는 편이 좋습니다. 또 문서는 처음 쓰는 스키마에 문법 컴파일 지연이 생기고 컴파일 결과를 마지막 사용 후 24시간 캐시한다고 설명하는데, 게이트웨이에서 새 스키마와 같은 스키마 반복 두 번을 재 보니 4.23초, 3.33초, 4.28초로 네트워크 편차에 묻혀 차이가 보이지 않았습니다.

## Claude 구조화된 출력 FAQ: Bedrock·스트리밍·thinking·JSON 모드

### Claude에도 JSON 모드가 있나요?

Claude API에는 OpenAI의 JSON 모드처럼 켜고 끄는 별도 옵션이 없고, 2026년 10월 30일 기준 `output_config.format`에 JSON 스키마를 넣는 구조화된 출력이 그 역할을 합니다. OpenAI 쪽의 `text.format`과 함수 호출 선택은 [OpenAI Structured Outputs와 Function Calling 차이: JSON 응답과 도구 실행 선택법](https://blog.laozhang.ai/ko/posts/structured-outputs-vs-function-calling)에서 다룹니다.

### Amazon Bedrock이나 Vertex AI에서도 쓸 수 있나요?

Anthropic 구조화된 출력 문서는 2026년 10월 30일 기준 Claude API, Claude Platform on AWS, Google Cloud의 Claude, Amazon Bedrock(InvokeModel 포함), Microsoft Foundry에서 정식 지원한다고 적고 있습니다. 지원 모델 목록에는 Fable·Mythos 계열, Opus 5.5·5, Sonnet 5.5·5, Haiku 5.5·4.5, Opus 4.5~4.8, Sonnet 4.5·4.6이 올라 있고 Claude 3.x 계열은 없습니다. Managed Agents에서는 쓸 수 없습니다.

### 스트리밍이나 Message Batches와 함께 쓸 수 있나요?

Anthropic 문서상 둘 다 됩니다. 스트리밍은 일반 응답처럼 조각으로 오므로 전체를 모은 뒤에 파싱하고, Message Batches로 보낸 요청도 같은 스키마 규칙을 따릅니다. 반면 Citations를 켠 요청에 `output_config.format`을 넣으면 400이 납니다.

### extended thinking과 함께 쓰면 thinking도 JSON이 되나요?

아닙니다. Anthropic 문서는 스키마 문법이 Claude의 최종 출력에만 적용되고 thinking, 도구 호출, 도구 결과에는 적용되지 않는다고 설명합니다. 2026년 10월 30일 게이트웨이에서 `claude-sonnet-5-5`에 `thinking: {"type": "adaptive"}`와 `output_config.effort: "low"`를 함께 보내자 thinking 블록 없이 유효한 JSON `text` 블록 하나만 돌아왔습니다.

### Claude Code나 Agent SDK의 outputFormat도 같은 기능인가요?

다른 메커니즘입니다. [Claude Agent SDK 구조화된 출력 문서](https://code.claude.com/docs/en/agent-sdk/structured-outputs)에 따르면 `query()`에 `outputFormat`(TypeScript)이나 `output_format`(Python)으로 스키마를 넘기면 SDK가 결과를 검증하고 맞지 않으면 다시 요청하며, 재시도 한도를 넘으면 `error_max_structured_output_retries` 오류로 끝납니다.

### 스키마에 개인정보를 넣어도 되나요?

ZDR(Zero Data Retention) 계약에서도 프롬프트와 출력은 저장되지 않지만, JSON 스키마 자체는 컴파일 캐시를 위해 마지막 사용 후 최대 24시간 보관됩니다. 그래서 Anthropic 문서는 HIPAA 대상 데이터를 다룰 때 속성 이름, `enum`·`const` 값, 정규식 패턴에 PHI를 넣지 말라고 안내합니다.

## 참고 자료

이 글이 링크한 외부 페이지를 본문에 나온 순서대로 정리했습니다. 마지막 업데이트: 2026-10-30.

- [Anthropic Claude 구조화된 출력 문서](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) (platform.claude.com)
- [strict tool use 문서](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use) (platform.claude.com)
- [Anthropic 가격 페이지](https://platform.claude.com/docs/en/about-claude/pricing) (platform.claude.com)
- [Batch API의 50% 할인](https://platform.claude.com/docs/en/build-with-claude/batch-processing) (platform.claude.com)
- [Claude Agent SDK 구조화된 출력 문서](https://code.claude.com/docs/en/agent-sdk/structured-outputs) (code.claude.com)
