chatgpt api json 출력 — json mode vs 스키마
ChatGPT API(OpenAI API)에서 JSON으로 답을 받는 방법은 두 가지입니다. JSON mode({"type": "json_object"})는 "문법상 올바른 JSON"만 보장하고, Structured Outputs({"type": "json_schema", "strict": true, ...})는 내가 준 스키마의 키·타입까지 지키게 합니다. 공식 문서는 가능하면 Structured Outputs를 쓰라고 권하고, 파이썬이라면 client.responses.parse(text_format=모델클래스) 한 번으로 끝납니다. 아래 예제는 openai 파이썬 SDK 3.19.2로 직접 돌려 확인했습니다.
1. json mode와 structured outputs 차이
공식 문서의 비교표를 옮기면 이렇습니다.
| Structured Outputs | JSON mode | |
|---|---|---|
| 올바른 JSON | 보장 | 보장 |
| 스키마(키·타입) 준수 | 보장 | 보장 안 함 |
| 쓸 수 있는 모델 | gpt-4o-mini, gpt-4o-2024-08-06 이후 |
gpt-3.5-turbo, gpt-4-*, gpt-4o-* 등 더 넓음 |
| 켜는 법(Responses API) | text.format에 type: "json_schema" + strict: true + schema |
text.format에 type: "json_object" |
JSON mode는 {"name": "키보드"}를 원했는데 {"product": "키보드"}가 와도 "올바른 JSON"이니 통과입니다. 필드가 빠지거나 숫자가 문자열로 와도 막아 주지 않습니다. 그래서 받는 쪽 코드가 키를 믿고 꺼내 쓴다면 Structured Outputs가 맞습니다.
2. chatgpt api json schema로 받기 — 파이썬 예제
파이썬 SDK는 Pydantic 모델을 그대로 스키마로 바꿔 줍니다. 공식 문서의 예제 형태를 따른 코드입니다.
from typing import Optional
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI() # 환경변수 OPENAI_API_KEY 사용
class Product(BaseModel):
name: str
price: int
tags: list[str]
discount: Optional[int]
response = client.responses.parse(
model="gpt-6-astra",
input="무선 기계식 키보드, 59000원, 할인 없음 — 상품 정보를 뽑아 줘",
text_format=Product,
)
product = response.output_parsed # Product 객체
print(product.name, product.price)
이 코드가 실제로 API에 무엇을 보내는지 궁금해서, 네트워크 대신 가짜 서버를 끼워 요청 본문을 가로채 봤습니다. SDK가 보낸 text 부분은 이랬습니다(title 줄은 생략).
{
"format": {
"type": "json_schema",
"strict": true,
"name": "Product",
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"price": { "type": "integer" },
"tags": { "type": "array", "items": { "type": "string" } },
"discount": { "anyOf": [ { "type": "integer" }, { "type": "null" } ] }
},
"required": ["name", "price", "tags", "discount"],
"additionalProperties": false
}
}
}
눈여겨볼 점이 세 가지입니다. strict: true와 additionalProperties: false를 SDK가 알아서 붙이고, Optional 필드도 required에 넣은 뒤 null을 허용하는 형태로 바꿉니다. 응답이 오면 output_parsed에 Product(name='키보드', price=59000, tags=['무선', '기계식'], discount=None)처럼 이미 파싱된 객체가 들어 있어서 json.loads를 따로 할 필요가 없습니다.
API 키가 아직 없다면 chatgpt api key 발급 글부터 보세요.
3. Chat Completions에서 response_format으로 받기
아직 Chat Completions API(chat.completions)를 쓰고 있다면 response_format에 같은 모델 클래스를 넘깁니다. 결과는 message.parsed에 들어 있습니다.
completion = client.chat.completions.parse(
model="gpt-6-astra",
messages=[{"role": "user", "content": "상품 정보를 뽑아 줘: 무선 키보드 59000원"}],
response_format=Product,
)
product = completion.choices[0].message.parsed
이때 SDK가 보내는 본문은 "response_format": {"type": "json_schema", "json_schema": {"name": ..., "schema": ..., "strict": true}} 꼴이었습니다. Responses API의 text.format과 자리만 다르고 내용은 같습니다.
4. json schema를 직접 쓸 때 규칙 — strict 모드 조건
SDK 없이 curl이나 다른 언어로 스키마를 직접 쓰면, SDK가 대신해 주던 것을 손으로 맞춰야 합니다. 공식 문서에 적힌 조건입니다.
| 규칙 | 내용 |
|---|---|
모든 필드 required |
선택 필드는 없다. 빼도 되는 값은 "type": ["string", "null"]처럼 null을 허용해 흉내 낸다 |
additionalProperties: false |
모든 객체마다 넣어야 한다(중첩 객체 포함) |
| 루트는 object | 최상위에 anyOf를 쓸 수 없다 |
| 안 되는 키워드 | allOf, not, dependentRequired, dependentSchemas, if/then/else |
| 크기 한도 | 속성 총 5000개, 중첩 10단계, enum 값 총 1000개 |
| 되는 제약 | 문자열 pattern·format(email, date 등), 숫자 minimum·maximum, 배열 minItems·maxItems |
문서에 따르면 strict: true인데 지원하지 않는 스키마를 보내면 에러가 납니다. 또 출력은 스키마에 적은 키 순서대로 나오니, 사람이 읽을 로그라면 중요한 키를 앞에 두는 것도 방법입니다.
curl https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-6-astra",
"input": "무선 키보드 59000원 — 상품 정보를 뽑아 줘",
"text": {
"format": {
"type": "json_schema",
"name": "product",
"strict": true,
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"price": { "type": "integer" }
},
"required": ["name", "price"],
"additionalProperties": false
}
}
}
}'
5. json mode 쓸 때 주의 — "JSON" 글자가 없으면 에러
옛 모델 때문에 JSON mode를 써야 한다면 한 가지를 꼭 지킵니다. 공식 문서 기준으로 대화 어딘가에 "JSON"이라는 글자가 없으면 API가 에러를 돌려줍니다. 지시 없이 켜면 모델이 공백만 끝없이 내다가 토큰 한도에 닿을 수 있어서 막아 둔 것입니다.
import json
response = client.responses.create(
model="gpt-6-astra",
input="상품 이름과 가격을 JSON으로 답해 줘: 무선 키보드 59000원",
text={"format": {"type": "json_object"}},
)
data = json.loads(response.output_text) # 키 이름은 보장되지 않는다
JSON mode는 파싱만 보장하므로 data["name"]이 있다는 보장은 없습니다. 이 방식을 쓴다면 받은 뒤에 Pydantic 같은 검증을 한 번 더 거치는 게 안전합니다.
6. chatgpt api json 안될때 — 확인할 것 3가지
① output_parsed가 None이다 → 거절(refusal)
모델이 안전상의 이유로 거절하면 응답이 스키마를 따르지 않고 refusal 항목으로 옵니다. 가짜 거절 응답을 넣어 보니 output_parsed는 None, 본문은 ResponseOutputRefusal(refusal="...", type='refusal')이었습니다. None 확인 없이 속성을 꺼내면 AttributeError가 납니다.
if response.output_parsed is None:
print("구조화된 답을 못 받음 — 거절(refusal)인지 response.output 확인")
else:
product = response.output_parsed
② JSON이 중간에 끊겼다 → 토큰 한도max_output_tokens에 걸리면 응답이 완성되지 않습니다. 공식 문서는 response.status == "incomplete"이고 incomplete_details.reason == "max_output_tokens"인지 확인하라고 안내합니다. 스키마가 크면 한도를 넉넉히 줍니다.
③ Pydantic 기본값이 안 먹는다count: int = 1처럼 기본값을 준 필드를 넣어 보니, SDK는 에러 없이 스키마에 "default": 1을 실은 채 그 필드를 required에 넣었습니다. 모든 필드가 필수라 모델은 그 값을 항상 직접 채우고, 파이썬 쪽 기본값이 쓰일 일이 없습니다. 비어도 되는 값이라면 기본값 대신 Optional로 두는 편이 의도에 맞습니다.
parse는 받은 JSON을 Pydantic으로 다시 검증합니다. 가짜 서버로 price에 문자열이 든 응답을 돌려주자 ValidationError가 났습니다. strict 모드라면 서버가 스키마를 지켜 주지만, 이 한 겹이 더 있다는 걸 알아 두면 예외 처리 범위를 정하기 쉽습니다.호출 자체가 429로 막힌다면 JSON 설정 문제가 아니라 한도·크레딧 문제입니다. openai api 429 에러 글을 보세요.
자주 묻는 질문 (FAQ)
Q. JSON mode와 Structured Outputs 중 뭘 써야 하나요?
모델이 지원한다면 Structured Outputs입니다. 공식 문서도 "가능하면 항상" Structured Outputs를 쓰라고 권합니다. JSON mode는 json_schema를 지원하지 않는 옛 모델을 써야 할 때의 대안입니다.
Q. 함수 호출(function calling)과는 뭐가 다른가요?
공식 문서의 구분은 이렇습니다. 모델을 DB 조회 같은 내 시스템의 도구에 연결하려면 함수 호출, 모델이 사용자에게 답하는 형태를 고정하려면 text.format(구조화 출력)입니다. 함수 호출의 형식과 실행 흐름은 openai function calling 예제 글에 따로 정리해 두었습니다.
Q. 스키마를 지켜도 내용이 틀릴 수 있나요?
네. 형태는 보장되지만 값의 정확성까지 보장되지는 않습니다. 공식 문서도 입력이 스키마와 전혀 상관없으면 모델이 억지로 채우다 지어낼 수 있으니, 그럴 때 빈 값이나 특정 문장을 돌려주라고 프롬프트에 적으라고 권합니다.
Q. 한글 값도 문제없나요?
네. 예제의 "키보드", ["무선", "기계식"]처럼 한글 값도 그대로 파싱됐습니다. 다만 한글은 토큰을 더 많이 쓰니 비용은 chatgpt api 요금 글의 계산법으로 따로 보세요.
마무리
정리하면, ChatGPT API에서 JSON을 받을 땐 Structured Outputs(json_schema + strict: true)가 기본이고, 파이썬이라면 Pydantic 모델을 text_format(Responses) 또는 response_format(Chat Completions)에 넘기면 됩니다. 스키마를 직접 쓸 땐 "모든 필드 required, 모든 객체에 additionalProperties: false" 두 가지를 먼저 확인하고, 받는 쪽에서는 거절(None)과 토큰 한도(incomplete)를 처리해 두세요.
SDK와 API는 자주 바뀝니다. 이 글은 openai 파이썬 SDK 3.19.2와 2026년 9월 26일 공식 문서 기준입니다.
📚 참고 출처 (2026년 9월 26일 확인 · openai-python 3.19.2)
· Structured model outputs — OpenAI API

COMMENTS