계층적 스키마 설계와 검증
실무의 데이터는 평평하지 않다. 한 사람에게 여러 주소가 있고, 한 주문에 여러 항목이 딸리며, 한 분석 결과에 요약과 근거 목록이 함께 들어간다. 구조화 출력은 객체 안에 객체를, 배열 안에 객체를 중첩해 이런 계층을 표현할 수 있다. 다만 모든 JSON Schema 키워드가 지원되는 것은 아니며, 어떤 제약은 조용히 무시되고 어떤 제약은 다른 계층에서 검증된다. 시험은 바로 이 경계, 즉 "무엇이 모델 디코딩에서 강제되고 무엇이 그렇지 않은가"를 집요하게 묻는다.
중첩 객체와 배열
객체 안에 객체를 넣을 때도 규약은 동일하다. 안쪽 객체에도 properties, required, additionalProperties false를 모두 갖춰야 한다. 배열은 items로 원소의 스키마를 지정하며, 원소가 객체라면 그 객체 역시 같은 규약을 따른다.
# /examples/15/nested_schema.pyimport anthropicimport jsonclient = anthropic.Anthropic()schema = { "type": "object", "properties": { "summary": {"type": "string"}, "customer": { "type": "object", "properties": { "name": {"type": "string"}, "tier": {"type": "string", "enum": ["free", "pro", "enterprise"]} }, "required": ["name", "tier"], "additionalProperties": False }, "issues": { "type": "array", "items": { "type": "object", "properties": { "title": {"type": "string"}, "severity": {"type": "string", "enum": ["low", "medium", "high"]} }, "required": ["title", "severity"], "additionalProperties": False } } }, "required": ["summary", "customer", "issues"], "additionalProperties": False}response = client.messages.create( model="claude-opus-4-8", max_tokens=2048, messages=[ {"role": "user", "content": "엔터프라이즈 고객 한도윤의 로그인 장애와 청구 오류 문의를 정리하라."} ], output_config={"format": {"type": "json_schema", "schema": schema}},)text = next(b.text for b in response.content if b.type == "text")data = json.loads(text)print(len(data["issues"]))
조합을 위한 키워드도 지원된다. 여러 형태 중 하나를 허용하는 anyOf, 모두를 만족해야 하는 allOf, 그리고 반복되는 하위 스키마를 정의해 재사용하는 ref가 그것이다. 같은 주소 객체가 청구지와 배송지 두 곳에서 쓰인다면, ref로 두 군데에서 참조하는 편이 깔끔하다.
지원되지 않는 제약: 가장 큰 함정
여기가 시험에서 가장 자주 나오는 함정 지점이다. 구조화 출력은 다음을 지원하지 않는다.
- 재귀 스키마(자기 자신을 참조하는 무한 깊이 구조)
- 수치 제약: minimum, maximum, multipleOf
- 문자열 제약: minLength, maxLength
- 복잡한 배열 제약
- false 이외의 additionalProperties
핵심은 이 제약들을 스키마에 넣어도 모델 디코딩 단계에서 강제되지 않는다는 점이다. 파이썬과 타입스크립트 SDK는 이런 미지원 키워드를 API로 보내기 전에 스키마에서 제거하고, 대신 응답을 받은 뒤 클라이언트 측에서 검증한다. 즉 "minimum: 0을 넣었으니 음수는 절대 안 나온다"는 가정은 틀렸다. 모델은 음수를 생성할 수 있고, 그 값을 걸러내는 일은 SDK의 사후 검증 또는 작성자의 코드가 맡는다. 시험에서 "스키마에 maxLength를 지정했는데 더 긴 문자열이 나왔다"는 증상이 나오면, 정답은 그것이 디코딩에서 강제되지 않는 제약이라는 점을 짚고 애플리케이션 측 검증으로 처리하는 선택지다. 길이나 범위를 정말 강하게 유도하고 싶다면, 스키마 제약이 아니라 프롬프트의 지시나 description으로 보조한다.
지원되는 문자열 format은 따로 있다. date-time, date, time, email, uri, uuid, ipv4, ipv6 같은 형식은 인식된다. 다만 이것은 값의 의미를 좁히는 형식 지정이지, 길이·범위 제약과는 다른 범주다.
도구 입력 검증: strict
구조화 출력의 또 다른 적용 지점은 도구 호출이다. 도구의 input_schema에 strict를 true로 두면, 모델이 그 도구를 호출할 때 만들어 내는 입력 인자가 스키마를 반드시 만족하도록 보장된다. output_config.format이 최종 응답 형식을 강제한다면, strict 도구는 도구 인자 형식을 강제한다. 둘은 같은 제약 디코딩 메커니즘의 서로 다른 적용 면이다.
# /examples/15/strict_tool.pyimport anthropicclient = anthropic.Anthropic()response = client.messages.create( model="claude-opus-4-8", max_tokens=1024, messages=[ {"role": "user", "content": "3월 15일 도쿄행 항공권을 2명으로 예약해 줘."} ], tools=[{ "name": "book_flight", "description": "목적지로 가는 항공권을 예약한다", "strict": True, "input_schema": { "type": "object", "properties": { "destination": {"type": "string"}, "date": {"type": "string", "format": "date"}, "passengers": {"type": "integer", "enum": [1, 2, 3, 4, 5, 6, 7, 8]} }, "required": ["destination", "date", "passengers"], "additionalProperties": False } }],)
strict 도구에도 같은 규약과 같은 미지원 제약이 그대로 적용된다. enum으로 passengers를 1에서 8 사이 정수로 닫은 것은 minimum/maximum이 강제되지 않기 때문에 택한 우회다. 범위를 닫힌 열거로 표현할 수 있다면 enum이, 그럴 수 없다면 사후 검증이 답이라는 점을 함께 기억한다.
정리
- 중첩 객체와 배열 원소에도 properties, required, additionalProperties false 규약이 똑같이 적용된다. 안쪽 객체에서 규약을 빠뜨린 선택지는 오답이다.
- anyOf, allOf, ref는 지원되며, 반복되는 하위 구조는 ref로 재사용한다.
- 재귀 스키마, 수치 제약(minimum/maximum/multipleOf), 문자열 길이 제약(minLength/maxLength)은 지원되지 않는다. 이 함정이 가장 자주 출제된다.
- 미지원 제약은 디코딩에서 강제되지 않는다. SDK는 이를 제거 후 클라이언트 측에서 사후 검증하므로, 길이·범위 보장이 필요하면 enum이나 애플리케이션 검증으로 처리한다.
- 도구 입력은 input_schema에 strict true로 강제한다. output_config.format(응답)과 strict(도구 인자)는 같은 메커니즘의 다른 적용 면이며, 미지원 제약 목록도 동일하다.