CATEGORY

카테고리 (672)
AI (70)
Language & Specs (260)
FrameWork (36)
Library (20)
App (41)
Git (10)
Build & Dependency (2)
AWS (15)
DataBase (45)
OS (33)
Tool (17)
IT (120)
SEEMINGLY ONLINE

Seemingly
Online

이모저모 방방곡곡 두루두루 개발지식 저장소

RECENT POSTS

AI/ChatGPT

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로 두는 편이 의도에 맞습니다.

💡 SDK의 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