튜토리얼

Gemini API에서 JSON을 출력하는 방법: Structured Output과 JSON Schema 가이드

모델에게 “JSON만 출력하라”고 부탁하지 마세요. Structured Output으로 형태를 고정하고 JSON Schema로 필드를 제약해야 다운스트림 코드가 안정적으로 파싱합니다.

Gemini에게 JSON을 요청했는데 서문이 붙거나, 쉼표가 빠지거나, 필드 이름이 더 친근하게 바뀝니다. 프롬프트에 「JSON만 출력」이라고 쓰면 확률은 낮아질 뿐, 계약은 아닙니다. Structured Output은 계약을 디코딩 단계로 옮깁니다. MIME 타입을 선언하고 스키마를 붙이면 모델이 그 모양으로 토큰을 냅니다. 이 가이드를 읽으면 JSON 모드와 Schema 모드를 고르고, 동작하는 요청을 쓰고, 그래도 로컬에서 검증할 수 있습니다.

구조화 출력이 필요한 이유

이후 단계가 JSON.parse를 호출하는 순간, 실패는 문구 문제가 아닙니다. 파이프라인 전체가 멈춥니다. 분류기는 고정 열거형이 필요하고, 추출기는 안정적인 키가 필요하며, 도구 호출은 파라미터 객체가 필요합니다. 자유 형식 답변은 비쌉니다. JSON이 한 번 깨지면 재시도, 로그, 또는 사용자의 클릭이 또 필요합니다.

더 조용한 실패는 「파싱은 되지만 모양이 틀린다」입니다. 당신은 items가 배열이길 기대했는데 객체를 받았고, score가 숫자이길 기대했는데 "0.9"를 받았습니다. 코드는 undefined를 읽고, 버그는 훨씬 뒤에서 터집니다. Structured Output이 고치는 것은 모양이지 진실이 아닙니다. 적법하고 스키마에 맞는 JSON을 받을 뿐, 분류가 맞다는 보장은 없습니다. 프로덕션에는 여전히 비즈니스 검사가 필요합니다. 파싱만 조용해질 뿐입니다.

공식 능력 안내는 Gemini Structured output 문서에서 확인하세요. Google은 더 넓은 JSON Schema 키워드와 속성 순서도 발표했습니다. Structured Outputs 업데이트를 읽어 보세요. 스키마를 쓰기 전에 Understanding JSON Schema를 훑어 두면, 「스펙이 허용하는 키워드」와 「이 모델이 실제로 강제하는 키워드」를 혼동하지 않습니다.

JSON 모드와 Schema 모드

스위치를 두 개로 생각하세요. 첫 번째는 「이 문자열이 JSON으로 파싱된다」만 약속합니다. 두 번째는 「이 JSON이 당신이 선언한 스키마와 맞는다」를 약속합니다. 코드가 이름 붙은 필드를 읽으면 두 번째를 쓰세요. 첫 번째는 모델이 키까지 만들어 내는 탐색적 추출에만 쓰세요.

단계 무엇을 설정하는가 실제로 얻는 것
JSON 모드 오직 responseMimeType: application/json 대개 유효한 JSON입니다. 이름과 중첩은 여전히 모델이 고릅니다
Schema 모드 MIME 타입 + responseSchema또는responseJsonSchema 모양, 타입, 필수 필드, 열거형이 스키마를 따릅니다

JSON 모드만 쓰면 문서도 여전히 강한 힌트로 취급하고, 깨진 출력의 작은 위험을 남깁니다. 「항상 객체로 파싱된다」에 가까워지려면 스키마도 보내세요. 스키마는 입력 토큰에 포함되므로, 같은 설명을 프롬프트에 또 붙이지 마세요. 중복은 품질과 할당량을 깎습니다.

responseSchema 또는 responseJsonSchema

responseSchema는 OpenAPI 3.0 스타일 스키마 부분집합을 씁니다. REST 타입 이름은 종종 대문자입니다. 예: OBJECT, STRING. 평평한 객체, 열거 분류, propertyOrdering로 키 순서를 고정하는 데 맞습니다. $ref / $defs는 이해하지 못하므로, 재귀 트리와 공유 정의는 인라인해야 하고 곧 크기가 폭발합니다.

responseJsonSchema는 Gemini 2.5 이상을 대상으로 하며 표준 JSON Schema에 더 가깝습니다. anyOf, $ref, minimum / maximum, additionalProperties, type: null, prefixItems 등을 다룹니다. Pydantic이나 Zod에서 스키마를 생성하면 마찰이 줄어듭니다. 최신 모델은 선언된 키 순서를 유지하므로 로그 diff와 골든 테스트에 도움이 됩니다.

경험 규칙 세 가지. 평평한 분류/추출: 어느 필드든 됩니다. 재귀, 공유 정의, 유니온: responseJsonSchema를 고르세요. 필드 순서를 고정해야 하면 엔드포인트가 여전히 propertyOrdering를 존중하는지 확인하세요. 모든 API 표면이 같다고 가정하지 마세요.

스키마 작성법

실제로 읽을 객체를 설명하세요. 완전한 세계 모델이 아닙니다. 선택 필드가 하나 늘 때마다 쓰레기를 채울 기회가 생깁니다. 필수 이름은 required에, 열거형은 enum에, 숫자 범위는 minimum / maximum에 넣으세요. 속성에 description를 다는 편이 프롬프트에서 다시 설명하는 것보다 안정적인 경우가 많습니다. 제약이 디코딩과 함께 가기 때문입니다.

아래 스키마는 티켓 분류를 모델합니다. category는 세 값 중 하나, priority는 정수, summary는 문자열입니다. Schema 모드가 맡아야 하는 것은 이것입니다. 모양이지, 티켓이 정말로 긴급인지는 아닙니다.

{
  "type": "object",
  "properties": {
    "category": {
      "type": "string",
      "enum": ["billing", "bug", "feature"],
      "description": "Ticket category"
    },
    "priority": {
      "type": "integer",
      "minimum": 1,
      "maximum": 5
    },
    "summary": {
      "type": "string"
    }
  },
  "required": ["category", "priority", "summary"],
  "additionalProperties": false
}

배열 원소는 items로 적습니다. 튜플처럼 길이가 고정된 목록은 JSON Schema 경로의 prefixItems를 보세요. 「required에 없다」를 nullable로 취급하지 마세요. null를 명시적으로 허용하지 않으면 모델이 키를 생략할 수 있는데, 코드는 obj.field가 항상 있다고 가정합니다.

중첩 객체는 인라인하세요. $defs + $ref는 같은 구조가 세 번째 나타나거나 트리 노드가 자신을 참조할 때만 쓰세요. 이른 추상화는 거절을 읽기 어렵게 만듭니다. 서버가 스키마를 거부하면 펼쳐진 덩어리를 디버깅하게 됩니다.

실무의 Python과 JavaScript

예제는 흔한 공식 SDK 형태입니다. 모델 id는 프로젝트에 실제로 있는 2.5 / 최신 SKU로 바꾸세요. 샘플 이름을 고정된 프로덕션 핀으로 취급하지 마세요.

Python: MIME 타입 + JSON Schema

from google import genai

client = genai.Client()
schema = {
    "type": "object",
    "properties": {
        "category": {"type": "string", "enum": ["billing", "bug", "feature"]},
        "priority": {"type": "integer"},
        "summary": {"type": "string"},
    },
    "required": ["category", "priority", "summary"],
}

response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="Classify this ticket: invoice PDF cannot be downloaded.",
    config={
        "response_mime_type": "application/json",
        "response_json_schema": schema,
    },
)
print(response.text)

팀이 이미 Pydantic으로 모델링한다면 Model.model_json_schema()response_json_schema에 넘기고, model_validate_json(response.text)로 로컬에서 한 번 더 검사하세요. 1층은 API 모양이고, 2층은 합법처럼 보이지만 말도 안 되는 값(우선순위 99 같은)을 타입 시스템이 거절하는 일입니다.

JavaScript: generationConfig

const response = await ai.models.generateContent({
  model: "gemini-2.5-flash",
  contents: "Classify this ticket: invoice PDF cannot be downloaded.",
  config: {
    responseMimeType: "application/json",
    responseJsonSchema: {
      type: "object",
      properties: {
        category: { type: "string", enum: ["billing", "bug", "feature"] },
        priority: { type: "integer" },
        summary: { type: "string" },
      },
      required: ["category", "priority", "summary"],
    },
  },
});
const data = JSON.parse(response.text);

REST 호출은 같은 필드를 generationConfig에 둡니다. OpenAPI 스타일 responseSchema는 일부 엔드포인트에서 여전히 대문자 타입을 씁니다. JSON Schema의 소문자 object와 섞지 마세요. 바로 JSON.parse로 파싱하세요. 펜스 코드 블록을 정규식으로 긁지 마세요. JSON MIME을 선언했으니 본문 전체를 JSON으로 취급하세요.

흔한 함정

  • 구조를 두 번 쓰면 — 한 번은 프롬프트, 한 번은 스키마 — 모델이 두 설명 사이에서 흔들립니다.
  • 과대한 스키마: 깊은 중첩, 깊은 $ref, 또는 넓은 anyOf는 거절되거나 약하게만 강제될 수 있습니다. 작은 객체부터 내보내고, 그다음 키우세요.
  • 진실을 스키마에 외주 주지 마세요. 열거형은 집합을 제한할 뿐, 잘못된 선택을 막지 않습니다. 중요 경로에는 샘플링이나 규칙을 더하세요.
  • 로컬 타입이 요청 스키마와 어긋납니다. Pydantic/Zod를 바꾸고 페이로드 스키마를 잊으면, 프로덕션에서 키가 조용히 늘거나 옛 키가 빠집니다.
  • 해피 패스만 테스트하지 마세요. 빈 배열, nullable 필드, 긴 문자열, 불법 열거형(차단되어야 함)을 넣으세요.

한 가지 더: 로그에 response.text만 남기지 마세요. 모델 id, 스키마 해시, 프롬프트 버전을 기록하세요. 구조화 출력 장애는 대개 「기본값이 바뀌었다」거나 「작은 스키마 수정이 거절됐다」입니다. 이 셋이 없으면 「어제까지는 됐는데」라고 장담하게 됩니다.

출고: 검증, 비교, 재시도

API 본문을 신뢰할 수 없는 바이트로 취급하세요. 파싱하고, 같은 스키마로 검증한 뒤, 내부 타입으로 매핑하세요. 실패하면 원문을 로그하고(비밀은 가리고) 재시도와 저하 중 고르세요. 완전히 다른 스키마로 재시도하지 마세요. 모델 노이즈와 움직이는 계약을 구분할 수 없습니다.

디버깅 중에는 샘플을 사이트 도구에 붙여 넣으세요. JSON 포맷로 중첩을 보고, JSON 검증로 구문을 잡은 뒤, 계약을 JSON Schema에 넣어 인스턴스가 통과하는지 보세요. 필드가 나타나거나 사라지면 로그를 눈으로 훑지 말고 JSON Diff로 두 응답을 비교하세요.

추출 파이프라인을 설계할 때 「이상적인 출력」을 손으로 하나 쓰고, Schema 도구로 타입을 추론한 뒤, 그 스키마를 Gemini 요청에 다시 붙이세요. 계약은 한곳에 삽니다. 채팅 기록의 구두 합의가 아니라, 테스트할 수 있는 문서입니다.

자주 묻는 질문 FAQ

application/json만으로 충분한가요?

탐색에는 괜찮습니다. 코드가 고정 필드를 읽는 순간 스키마도 보내세요. 그렇지 않으면 JSON 모양의 산문을 얻지, API를 얻지 않습니다.

Structured Output이 함수 호출을 대체할 수 있나요?

아니요. 함수 호출은 모델이 도구를 고르고 인자를 채우게 합니다. Structured Output은 이번 답의 모양을 제약합니다. 코드를 실행하거나 외부 API를 치려면 도구를 쓰세요. 타입이 있는 덩어리 하나면 Structured Output으로 왕복을 건너뛰세요.

스키마가 거절된 이유는?

대개 그 엔드포인트에서 지원하지 않는 키워드, 너무 깊은 재귀, 또는 responseSchema와 responseJsonSchema 방언을 섞은 경우입니다. 필드 세 개의 객체 하나로 줄여 동작하는지 증명한 뒤 더하세요.

출력이 항상 맞나요?

아니요. 모양은 유효한데 사실이 틀릴 수 있습니다. 금액, 이메일, 티켓 분류에는 여전히 규칙이나 사람 샘플링이 필요합니다.

요약과 다음 단계

믿을 수 있는 JSON은 더 긴 「JSON만 출력해 주세요」에서 오지 않습니다. MIME 타입과 스키마에서 옵니다. 평평한 작업은 responseSchema또는responseJsonSchema 어느 쪽이든 괜찮습니다. $ref, 유니온, 또는 Pydantic/Zod에서 생성한 스키마는 후자를 쓰세요. 그래도 로컬에서 검증하고, 실패는 포맷, Schema, Diff로 회귀 테스트로 만드세요.

다음 단계: 가장 잘 깨지는 엔드포인트를 「스키마 하나, 요청 하나, 로컬 validate 하나」로 만드세요. 그 경로를 먼저 조용히 한 뒤, 패턴을 복사하세요.