1 왜 JSON Schema를 알아야 하는가
JSON Schema는 직접 작성하지 않아도 거의 모든 현대 API 도구가 내부적으로 사용한다.
| 도구·패턴 | JSON Schema 활용 |
|---|---|
| Pydantic | model.model_json_schema()로 자동 생성. 검증·시리얼라이즈에 사용 |
| FastAPI | /docs (Swagger UI)에 OpenAPI(JSON Schema 기반) 자동 노출 |
| OpenAPI 3.0+ | API schema 정의의 근간. 클라이언트 SDK 자동 생성 |
| MINERVA 12-1 Snapshot 테스트 | LLM 응답의 구조 회귀를 jsonschema로 검증 |
| JSON 설정 파일 검증 | .vscode/settings.json 등 IDE가 JSON Schema로 자동완성·오류 표시 |
| 데이터 파이프라인 | Airflow·dbt·Great Expectations가 dataset schema 검증에 사용 |
본 글은 그 토대를 정리한다. 직접 손으로 schema를 작성할 일이 적어도 자동 생성된 schema를 읽고 디버깅할 수 있어야 한다.
2 정의와 기본 구조
JSON 데이터의 구조·타입·제약을 선언적으로 기술하는 JSON 형식 표준. 데이터 검증·문서화·코드 생성에 사용된다.
- 선언적: “어떻게 검증할지”가 아니라 “어떤 모양이어야 하는지”만 기술
- JSON 자체로 표현: schema도 JSON, 데이터도 JSON — 같은 형식
- 버전: Draft 4·6·7·2019-09·2020-12 (가장 널리 쓰이는 건 Draft 7)
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name"]
}이 schema는 다음 데이터를 통과시킨다:
다음을 거부:
3 기본 키워드
3.1 type
{"type": "string"} ← 문자열만
{"type": "integer"} ← 정수
{"type": "number"} ← 정수 또는 실수
{"type": "boolean"} ← true/false
{"type": "null"} ← null만
{"type": "array"} ← 배열
{"type": "object"} ← 객체
{"type": ["string", "null"]} ← 둘 중 하나 (Optional)type 단일 키워드 또는 배열로 여러 타입 허용.
3.2 object — properties + required
{
"type": "object",
"properties": {
"id": {"type": "integer"},
"name": {"type": "string"},
"email": {"type": ["string", "null"]}
},
"required": ["id", "name"] ← email은 optional
}required에 명시한 키만 필수, 그 외는 선택. 기본 동작은 정의 안 한 키도 허용한다 — 추가 키를 막으려면 additionalProperties: false.
3.3 문자열 제약
{
"type": "string",
"minLength": 1,
"maxLength": 100,
"pattern": "^[a-zA-Z0-9_]+$", ← 정규식
"format": "email" ← 미리 정의된 형식 (email, date, uri 등)
}format은 hint다 — validator마다 강제 여부가 다름. pattern이 더 엄격.
3.4 숫자 제약
3.5 배열 — items
{
"type": "array",
"items": {"type": "string"}, ← 모든 요소가 string
"minItems": 1,
"maxItems": 10,
"uniqueItems": true ← 중복 금지
}{
"type": "array",
"items": [
{"type": "string"},
{"type": "integer"}
], ← 첫 요소 string, 둘째 integer (tuple validation, Draft 4~7)
"additionalItems": false ← 그 이상 요소 금지
}Draft 2020-12부터는 prefixItems로 명시.
4 변형 — allOf·anyOf·oneOf·not
여러 schema를 조합한다.
// allOf — 모두 만족
{
"allOf": [
{"type": "object", "properties": {"id": {"type": "integer"}}},
{"type": "object", "properties": {"name": {"type": "string"}}}
]
}// anyOf — 적어도 하나 만족
{
"anyOf": [
{"type": "string", "format": "email"},
{"type": "string", "format": "uri"}
]
}// oneOf — 정확히 하나 만족 (둘 이상 만족하면 실패)
{
"oneOf": [
{"type": "object", "properties": {"phone": {"type": "string"}}, "required": ["phone"]},
{"type": "object", "properties": {"email": {"type": "string"}}, "required": ["email"]}
]
}oneOf와 anyOf의 차이: oneOf는 정확히 하나만 만족. 둘 다 만족하면 실패. anyOf는 하나 이상 만족하면 OK.
5 $ref — 재사용
같은 schema를 여러 곳에서 쓰려면 $defs(또는 definitions)에 정의 후 $ref로 참조.
{
"$defs": {
"User": {
"type": "object",
"properties": {"id": {"type": "integer"}, "name": {"type": "string"}},
"required": ["id", "name"]
}
},
"type": "object",
"properties": {
"author": {"$ref": "#/$defs/User"},
"reviewer": {"$ref": "#/$defs/User"}
}
}$ref 값은 JSON Pointer 형식. #/$defs/User는 “이 문서 root의 $defs 안 User”.
외부 파일 참조도 가능:
6 enum과 const
7 conditional — if/then/else
{
"if": {"properties": {"type": {"const": "user"}}},
"then": {"required": ["name"]},
"else": {"required": ["service_id"]}
}type 필드가 “user”면 name 필수, 아니면 service_id 필수.
8 Pydantic이 JSON Schema 생성하는 방식
Pydantic 모델은 model_json_schema()로 schema를 자동 생성한다.
from pydantic import BaseModel, Field
from typing import Literal
class User(BaseModel):
id: int = Field(ge=0)
name: str = Field(min_length=1, max_length=100)
role: Literal["admin", "user", "guest"] = "user"
email: str | None = None
print(User.model_json_schema())출력:
{
"type": "object",
"title": "User",
"properties": {
"id": {"type": "integer", "minimum": 0, "title": "Id"},
"name": {"type": "string", "minLength": 1, "maxLength": 100, "title": "Name"},
"role": {
"enum": ["admin", "user", "guest"],
"title": "Role",
"type": "string",
"default": "user"
},
"email": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Email",
"default": null
}
},
"required": ["id", "name"]
}자동으로 처리되는 것:
| Pydantic | 생성된 JSON Schema |
|---|---|
int + Field(ge=0) |
{"type": "integer", "minimum": 0} |
str + Field(min_length=1) |
{"type": "string", "minLength": 1} |
Literal["a", "b"] |
{"enum": ["a", "b"]} |
X \| None (또는 Optional[X]) |
{"anyOf": [..., {"type": "null"}]} |
Annotated[int, Field(ge=0)] |
{"type": "integer", "minimum": 0} |
| 중첩 모델 | $ref로 분리 |
이 자동 생성이 OpenAPI 문서·클라이언트 SDK·검증·테스트의 토대다. Pydantic 글에서 검증 측면을, 본 글에서 schema 측면을 다룬다.
9 OpenAPI 3.0+와의 관계
OpenAPI는 REST API 전체(엔드포인트 + 파라미터 + 응답 schema)를 기술하는 표준이다. schema 부분이 JSON Schema.
# openapi.yaml 일부
components:
schemas:
User: # 이 부분이 JSON Schema
type: object
properties:
id: {type: integer, minimum: 0}
name: {type: string, minLength: 1}
required: [id, name]
paths:
/users/{id}:
get:
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/User"FastAPI는 모든 라우터를 분석해 OpenAPI 문서를 자동 생성하고 /openapi.json·/docs에 노출. Pydantic 모델 → JSON Schema → OpenAPI까지 자동.
10 MINERVA Snapshot 테스트 — JSON Schema 검증
MINERVA 12-1편 고급 테스트 패턴이 LLM 응답 구조 회귀를 schema 검증으로 처리한다.
import pytest
from jsonschema import validate
ANSWER_SCHEMA = {
"type": "object",
"required": ["text", "citations", "run_id", "model"],
"properties": {
"text": {"type": "string", "minLength": 10},
"citations": {
"type": "array",
"items": {
"type": "object",
"required": ["index", "content"],
"properties": {
"index": {"type": "integer", "minimum": 1},
"content": {"type": "string"},
"score": {"type": ["number", "null"]},
},
},
},
"model": {"type": "string"},
"latency_ms": {"type": ["integer", "null"], "minimum": 0},
},
}
@pytest.mark.snapshot
def test_response_schema_for_canonical_query():
response = agent.run(Query(text="..."))
validate(instance=response.model_dump(), schema=ANSWER_SCHEMA)LLM은 비결정적이라 정확한 텍스트 일치 테스트는 불가능하지만, 구조(어떤 키가 어떤 타입으로 있는지)는 회귀를 잡을 가치가 있다 — 프롬프트 변경이 응답 schema를 깨뜨리는 사고를 사전에 catch.
11 Python에서의 사용
11.1 검증 — jsonschema 라이브러리
from jsonschema import validate, ValidationError
schema = {
"type": "object",
"properties": {"id": {"type": "integer"}, "name": {"type": "string"}},
"required": ["id", "name"],
}
try:
validate(instance={"id": 1, "name": "Alice"}, schema=schema)
print("Valid")
except ValidationError as e:
print(f"Invalid: {e.message}")Draft7Validator 등 validator 클래스도 직접 사용 가능 (커스텀 keyword 추가 등).
11.2 Pydantic으로 검증 (권장)
from pydantic import BaseModel, ValidationError
class User(BaseModel):
id: int
name: str
try:
user = User.model_validate({"id": 1, "name": "Alice"})
except ValidationError as e:
print(e.errors())jsonschema 대비 장점: 타입 힌트와 자동 통합, IDE 지원, 빠른 속도. 대부분의 Python API에서 jsonschema 직접 사용보다 Pydantic을 권장.
jsonschema를 직접 쓰는 케이스: - Pydantic 모델로 표현하기 어려운 복잡한 conditional 검증 - 외부 schema 파일을 그대로 사용해야 할 때 - 다른 언어 클라이언트와 schema 공유 (단일 진실)
12 자주 발생하는 오류 패턴
입력: {"id": 1, "evil_field": "..."} → 통과됨 (의도와 다름)
CORRECT:
{
"type": "object",
"properties": {"id": {"type": "integer"}},
"required": ["id"],
"additionalProperties": false
}JSON Schema 기본값은 정의 안 된 추가 키를 허용한다. 엄격 검증 시 additionalProperties: false.
입력: {} → 통과 (name 필수가 아니므로)
CORRECT:
required 배열에 명시하지 않으면 모든 필드는 optional. 헷갈리면 명시.
입력: "hello world" → 둘 다 만족 → oneOf 실패
CORRECT:
oneOf는 정확히 하나만 만족해야 한다. 의도가 “여러 조건 중 하나라도”면 anyOf.
검증기에 따라 "not-an-email"도 통과.
CORRECT:
format은 hint이고 강제 여부가 validator마다 다르다. 강제하려면 pattern 또는 validator의 format_checker 옵션.
{
"$defs": {
"Node": {
"type": "object",
"properties": {
"children": {"type": "array", "items": {"$ref": "#/$defs/Node"}}
},
"required": ["children"]
}
}
}무한 깊이 트리를 강제 — 실제 데이터로 만들 수 없음.
CORRECT:
{
"$defs": {
"Node": {
"type": "object",
"properties": {
"children": {"type": "array", "items": {"$ref": "#/$defs/Node"}}
}
}
}
}순환 참조 자체는 가능하지만 required로 무한 깊이 강제 안 함. 자식 배열을 optional로 두거나 maxDepth 제어.
13 정리
| 영역 | 핵심 |
|---|---|
| 기본 키워드 | type, properties, required |
| 제약 | minLength·pattern (string), minimum·multipleOf (number) |
| 변형 | allOf (모두), anyOf (하나 이상), oneOf (정확 하나), not |
| 재사용 | $defs + $ref (JSON Pointer) |
| 자동 생성 | Pydantic model_json_schema() → OpenAPI → 클라이언트 SDK |
| 검증 도구 | jsonschema (직접) 또는 Pydantic (권장) |
| 응용 | API 문서·snapshot 테스트·설정 파일 검증 |
14 응용 분야
| 사용처 | 본 글 절 |
|---|---|
| FastAPI 라우터 자동 문서 | Pydantic + OpenAPI |
| MINERVA 12-1 snapshot 테스트 | jsonschema validate + Schema |
| Pydantic 모델 정의 | Field 제약 → 자동 schema |
| OpenAPI에서 클라이언트 SDK 생성 | schema → openapi-generator |
_quarto.yml·tsconfig.json 자동완성 |
IDE가 schema 사용 |
| dbt·Great Expectations 데이터 검증 | dataset schema |
15 관련 주제
선행 학습
MINERVA 시리즈 응용
- MINERVA 고급 테스트 패턴 (12-1) – snapshot 테스트의 schema 검증
- MINERVA BaseAgent 계약 (02-0) – Pydantic 모델 → JSON Schema 자동 노출
- MINERVA FastAPI 서빙 (04) – /openapi.json·/docs 자동 생성
다른 카테고리 연결
- Python typing 심화 – Pydantic이 typing을 어떻게 schema로 변환하는가