JSON 스키마 정의
프롬프트로 "JSON으로만 답하라"고 지시해도 모델은 가끔 앞에 설명 문장을 붙이거나, 키 이름을 미묘하게 바꾸거나, 숫자를 문자열로 감싼다. 사람이 읽는다면 무시할 수 있는 차이지만, 응답을 코드로 파싱하는 순간 이런 흔들림은 곧장 예외로 이어진다. 구조화 출력은 이 문제를 근본적으로 다른 층위에서 해결한다. 프롬프트로 "부탁"하는 대신, API가 모델의 디코딩 과정에 스키마를 주입해 스키마를 위반하는 토큰 자체를 생성할 수 없게 만든다. 시험은 이 차이를 분명히 묻는다. "JSON 형식이 가끔 깨진다"는 증상에 대한 정답은 프롬프트에 더 강한 지시를 추가하는 선택지가 아니라, 스키마를 강제하는 구조화 출력으로 전환하는 선택지다.
output_config.format으로 형식을 강제한다
현행 Anthropic API에서 응답 형식은 messages.create의 output_config 안 format으로 지정한다. format의 type은 json_schema이며, schema 필드에 표준 JSON Schema 객체를 넣는다. 과거에 쓰이던 최상위 output_format 파라미터는 폐기되었으므로, 시험에서 output_format을 최상위에 두는 선택지가 보이면 함정이다. 정답은 output_config 안에 format을 중첩하는 형태다.
# /examples/15/raw_schema.pyimport anthropicimport jsonclient = anthropic.Anthropic()response = client.messages.create( model="claude-opus-4-8", max_tokens=1024, messages=[ {"role": "user", "content": "다음에서 정보를 추출하라. 김민수([email protected])가 Enterprise 요금제를 원함."} ], output_config={ "format": { "type": "json_schema", "schema": { "type": "object", "properties": { "name": {"type": "string"}, "email": {"type": "string"}, "plan": {"type": "string"}, "demo_requested": {"type": "boolean"} }, "required": ["name", "email", "plan", "demo_requested"], "additionalProperties": False } } },)text = next(b.text for b in response.content if b.type == "text")data = json.loads(text)print(data["name"])
출력은 여전히 응답의 텍스트 블록으로 돌아오지만, format을 지정했으므로 그 텍스트는 스키마를 만족하는 유효한 JSON임이 보장된다. 따라서 json.loads로 안전하게 파싱할 수 있다. 응답 본문에 코드 펜스나 인사말이 섞이는 일은 없다.
기본 타입과 두 가지 필수 규약
schema에 쓸 수 있는 기본 타입은 object, array, string, integer, number, boolean, null이다. 여기에 모든 객체가 반드시 지켜야 하는 두 규약이 있다.
첫째, 모든 객체에 additionalProperties를 false로 둔다. 스키마에 정의하지 않은 키를 모델이 임의로 추가하지 못하게 막는 장치이며, 구조화 출력에서 사실상 필수다. 둘째, required 배열에 객체의 모든 키를 명시한다. 구조화 출력에서는 일부만 채우는 부분 응답이 아니라 정의한 필드를 빠짐없이 채운 응답을 받는 것이 안전하기 때문이다. 시험에서 스키마 예시에 additionalProperties가 빠져 있거나 required가 누락된 선택지는 거의 정답이 아니다.
타입을 좁히는 가장 강력한 도구는 enum이다. 값이 정해진 집합에서만 나와야 하는 필드(분류 라벨, 우선순위, 상태)는 string에 enum을 더해 닫힌 선택지로 만든다. 프롬프트로 "셋 중 하나"라고 말하는 것보다 스키마로 enum을 못박는 편이 훨씬 견고하다.
SDK 헬퍼: messages.parse
원시 스키마를 직접 쓰는 대신, SDK가 제공하는 편의 메서드를 쓰면 스키마 생성과 검증·파싱이 한 번에 처리된다. 파이썬에서는 Pydantic 모델을 정의해 client.messages.parse에 output_format으로 넘기고, 결과는 response.parsed_output에서 검증된 객체로 받는다.
# /examples/15/parse_helper.pyimport anthropicfrom pydantic import BaseModelclass ContactInfo(BaseModel): name: str email: str plan: str demo_requested: boolclient = anthropic.Anthropic()response = client.messages.parse( model="claude-opus-4-8", max_tokens=1024, messages=[ {"role": "user", "content": "김민수([email protected])가 Enterprise 요금제를 원함. 데모 요청함."} ], output_format=ContactInfo,)contact = response.parsed_outputprint(contact.name)
여기서 주의할 점은 두 방식의 시그니처가 다르다는 것이다. 원시 방식은 messages.create에 output_config={"format": {...}}를 넘기고, 헬퍼 방식은 messages.parse에 output_format=모델을 넘긴다. 둘을 뒤섞은 선택지(예: messages.create에 output_format을 직접 넘기는 형태)는 시험에서 흔한 함정이다. parse는 SDK 편의일 뿐이며, API 수준의 정식 파라미터는 어디까지나 output_config.format임을 기억한다.
정리
- 구조화 출력은 프롬프트로 형식을 부탁하는 것이 아니라, API가 디코딩에 스키마를 강제해 스키마 위반 토큰을 생성하지 못하게 막는다. "JSON이 가끔 깨진다"의 정답은 프롬프트 강화가 아니라 스키마 강제다.
- 응답 형식은 output_config 안 format(type은 json_schema)으로 지정한다. 폐기된 최상위 output_format을 쓰는 선택지는 함정이다.
- 모든 객체는 additionalProperties를 false로 두고 required 배열에 모든 키를 명시한다. 둘 중 하나라도 빠진 스키마 선택지는 대개 오답이다.
- 값이 정해진 집합인 필드는 string에 enum을 더해 닫는다. 프롬프트로 제한하는 것보다 스키마로 못박는 편이 견고하다.
- SDK 헬퍼 messages.parse는 output_format에 모델을 넘기고 parsed_output을 받는다. 원시 messages.create의 output_config.format과 시그니처가 다르므로 둘을 섞지 않는다.