JSON Schema 기초

Pydantic·OpenAPI·Snapshot 테스트가 모두 의존하는 검증 표준

JSON Schema는 JSON 데이터의 구조·타입·제약을 선언적으로 기술하는 표준이다. Pydantic이 자동으로 생성하고, FastAPI가 OpenAPI에 노출하며, MINERVA 12-1편 Snapshot 테스트가 응답 구조 회귀 검증에 사용한다. 본 글은 핵심 키워드(type·properties·required)부터 변형($ref·oneOf·allOf), 자주 발생 오류까지 정리한다.

Engineering
저자

Kwangmin Kim

공개

2026년 05월 06일

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 Schema

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는 다음 데이터를 통과시킨다:

{"name": "Alice", "age": 30}         OK
{"name": "Bob"}                      OK (age 선택)

다음을 거부:

{"age": 30}                          실패: name 필수
{"name": 42}                         실패: name은 string
{"name": "Bob", "age": -1}           실패: age >= 0

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.

{
  "type": "object",
  "properties": {"id": {"type": "integer"}},
  "additionalProperties": false       id 외의  금지
}

3.3 문자열 제약

{
  "type": "string",
  "minLength": 1,
  "maxLength": 100,
  "pattern": "^[a-zA-Z0-9_]+$",        정규식
  "format": "email"                     미리 정의된 형식 (email, date, uri 등)
}

format은 hint다 — validator마다 강제 여부가 다름. pattern이 더 엄격.

3.4 숫자 제약

{
  "type": "integer",
  "minimum": 0,
  "maximum": 100,
  "exclusiveMinimum": 0,               > 0 (미만)
  "multipleOf": 5                      5 배수만
}

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"]}
  ]
}
// not  만족하지 않아야
{"not": {"type": "string"}}            string이 아닌 모든 

oneOfanyOf의 차이: 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의 $defsUser”.

외부 파일 참조도 가능:

{"$ref": "common-types.json#/$defs/User"}

6 enum과 const

{"enum": ["pending", "running", "completed", "failed"]}
{"const": "v2"}                        정확히 "v2"만

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까지 자동.

@app.post("/agents/qna_chatbot/run")
def run(query: Query) -> Response:    # Pydantic 모델
    ...
# /openapi.json에 Query·Response의 JSON Schema가 자동 포함됨

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 라이브러리

pip install 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 자주 발생하는 오류 패턴

WRONG:

{
  "type": "object",
  "properties": {"id": {"type": "integer"}},
  "required": ["id"]
}

입력: {"id": 1, "evil_field": "..."} → 통과됨 (의도와 다름)

CORRECT:

{
  "type": "object",
  "properties": {"id": {"type": "integer"}},
  "required": ["id"],
  "additionalProperties": false
}

JSON Schema 기본값은 정의 안 된 추가 키를 허용한다. 엄격 검증 시 additionalProperties: false.

WRONG:

{
  "properties": {
    "name": {"type": "string"}
  }
}

입력: {} → 통과 (name 필수가 아니므로)

CORRECT:

{
  "properties": {
    "name": {"type": "string"}
  },
  "required": ["name"]
}

required 배열에 명시하지 않으면 모든 필드는 optional. 헷갈리면 명시.

WRONG:

{
  "oneOf": [
    {"type": "string"},
    {"type": "string", "minLength": 5}
  ]
}

입력: "hello world" → 둘 다 만족 → oneOf 실패

CORRECT:

{
  "anyOf": [
    {"type": "string"},
    {"type": "string", "minLength": 5}
  ]
}

oneOf는 정확히 하나만 만족해야 한다. 의도가 “여러 조건 중 하나라도”면 anyOf.

WRONG:

{"type": "string", "format": "email"}

검증기에 따라 "not-an-email"도 통과.

CORRECT:

{
  "type": "string",
  "format": "email",
  "pattern": "^[^@]+@[^@]+\\.[^@]+$"
}

format은 hint이고 강제 여부가 validator마다 다르다. 강제하려면 pattern 또는 validator의 format_checker 옵션.

WRONG:

{
  "$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 관련 주제

선행 학습

  • API 기초 – HTTP·JSON 토대
  • Pydantic – BaseModel이 자동으로 JSON Schema 생성

MINERVA 시리즈 응용

다른 카테고리 연결

Subscribe

Enjoy this blog? Get notified of new posts by email: