JSON Schema 검증: Node, Python, 브라우저에서 JSON 검증하기 (2026)
핵심 요약: JSON Schema는 JSON 데이터의 계약(contract)입니다. 필드 타입, 필수 키, 제약 조건을 선언하면 검증기(validator)가 어떤 JSON 문서든 그 계약을 따르는지 확인합니다. Node라면 가장 빠른 검증을 위해 Ajv를, Python이라면 이식성 있는 스키마를 위해 jsonschema 라이브러리를, 브라우저라면 폼과 설정에 즉각 피드백을 주기 위해 Ajv를 번들링하세요. 2026년 신규 프로젝트라면 Draft 2020-12를 고르면 됩니다.
이어지는 내용은 가장 작은 동작 예제, 세 런타임을 가로지르는 엔드투엔드 패턴, 그리고 “검증은 통과하는데 프로덕션이 데이터를 거부하는” 버그를 일으키는 실전 함정을 다룹니다.
JSON Schema란 무엇이고 무엇이 아닌가
한 문장 정의
JSON Schema는 다른 JSON 문서의 형태를 기술하는 JSON 문서입니다. 검증기는 스키마와 데이터를 읽어 들여 적합 여부를 알려주거나, 실패한 경로(path)를 반환합니다.
가장 작고 유용한 예제:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
}
{"name": "Alice"}는 통과합니다. {"age": 30}은 name이 빠져서 실패하고, {"name": 42}는 name이 문자열이 아니어서 실패합니다. 멘탈 모델은 이 정도면 충분합니다.
JSON Schema vs JSON 문법 검증
자주 혼동되는 두 가지 다른 문제입니다.
| 측면 | JSON 문법 검사 | JSON Schema 검증 |
|---|---|---|
| 무엇을 검사하나 | 적법한 JSON 문서인가? | 이 JSON이 계약과 일치하는가? |
| 잡아내는 것 | 누락된 콤마, 작은따옴표, 주석 | 잘못된 타입, 누락된 필수 필드, 범위를 벗어난 값 |
| 도구 | JSON.parse(), JSON 포맷터 | Ajv, jsonschema (Python), fastjsonschema |
| 언제 사용하나 | 가장 먼저, 파싱 전 | 파싱 직후, 비즈니스 로직 전 |
실무에서는 두 가지를 모두 씁니다. JSON 포맷터에서 페이로드를 정렬해 파싱이 되는지 본 다음, 스키마에 통과시켜 계약과 일치하는지 확인합니다.
JSON Schema vs JSONPath, JSON Patch, jq, TypeScript
이 문제 영역을 공유하는 다섯 가지 도구의 의사결정 매트릭스:
| 도구 | 답하는 질문 | 언제 사용하나 |
|---|---|---|
| JSON Schema | 이 JSON이 기대하는 구조와 일치하는가? | API 입력, 설정 파일, 폼 페이로드 검증 |
| JSONPath | 이 JSON에서 값을 어떻게 조회하나? | 중첩 필드 추출, 배치 읽기 |
| JSON Patch (RFC 6902) | A에서 B로의 차이를 어떻게 기술하나? | 협업 편집, 증분 동기화 |
| jq | 커맨드라인에서 JSON을 어떻게 처리하나? | 셸 스크립트, 로그 파이프라인, CI 검사 |
| TypeScript 타입 | 내 코드가 이 형태를 올바르게 사용하는가? | 단일 코드베이스 내부의 컴파일 타임 보장 |
결정적 차이는 이것입니다. JSON Schema는 런타임에 알 수 없는 데이터를 검증하고, TypeScript는 컴파일 타임에 이미 알려진 코드를 검증합니다. TypeScript는 서드파티 webhook이나 사용자가 붙여넣은 JSON에는 손을 댈 수 없습니다. 그 자리를 JSON Schema가 채웁니다. Zod와 Pydantic은 컴파일 타임 타입과 런타임 검증을 동시에 하는 중간 영역에 있으며, 아래에서 다룹니다.
JSON Schema vs OpenAPI
OpenAPI가 JSON Schema를 대체한다는 오해가 흔합니다. 그렇지 않습니다. OpenAPI는 내부적으로 JSON Schema를 사용해 요청 및 응답 본문을 기술하고, 그 위에 경로(path), 매개변수, 보안 스킴, 서버 URL을 얹습니다. 스키마는 데이터 형태에 대한 계약이고, OpenAPI는 그 계약을 감싸는 API 계약입니다.
| 측면 | JSON Schema | OpenAPI |
|---|---|---|
| 범위 | 단일 JSON 문서의 형태 | HTTP API 전체의 형태 |
| 의존성 | 없음 (스키마 자체가 자기완결적인 JSON) | 본문 정의를 위해 JSON Schema를 가져옵니다 |
| 버전 매핑 | Draft 7 / Draft 2019-09 / Draft 2020-12 | OpenAPI 3.0은 Draft 4의 부분집합을 사용하고, OpenAPI 3.1은 Draft 2020-12를 그대로 사용합니다 |
| 일반적인 용도 | 설정 파일, 메시지 봉투, 폼 검증, 단일 페이로드 계약 | REST API 설계, SDK 생성, 모의 서버, 계약 테스트 |
| 코드 생성 | 제한적 (quicktype 계열 도구 일부) | 성숙한 생태계 (openapi-generator, oapi-codegen, 벤더 SDK) |
| 계약 관리 | 형태 하나당 파일 하나, 라우팅 없음 | 경로, 오퍼레이션, 인증 흐름, 버전 엔드포인트가 한 문서 안에 |
관심 대상이 단일 문서일 때는 평범한 JSON Schema에 손을 뻗으세요. webhook 페이로드, 설정 파일, 큐 메시지, 폼이 그렇습니다. 기술해야 할 HTTP 표면이 없으므로 OpenAPI는 군더더기입니다.
HTTP API를 공개하면서 문서, SDK 생성, 모의 서버, 계약 테스트를 한 문서로 끌고 가고 싶을 때는 OpenAPI에 손을 뻗으세요. 먼저 스키마를 schemas/ 디렉터리 안에 독립적인 JSON Schema 파일로 정의한 다음, OpenAPI 문서에서 $ref로 참조하세요. 그래야 스키마가 API 맥락 밖에서도 재사용됩니다.
버전 매핑에서 팀들이 자주 발이 걸립니다. OpenAPI 3.0은 Draft 4의 부분집합을 사용하므로 3.0 문서 안에서는 prefixItems나 unevaluatedProperties 같은 Draft 2020-12 키워드를 쓸 수 없습니다. 생성기들이 조용히 무시합니다. OpenAPI 3.1은 Draft 2020-12의 상위집합이므로, 2020-12에서 유효한 것은 3.1에서도 유효합니다. 선택권이 있다면 OpenAPI 3.1을 목표로 잡고 어디에서나 Draft 2020-12 스키마를 작성하세요.
첫 JSON Schema 작성하기 (5분)
가장 먼저 알아야 할 키워드
아래 키워드만으로도 실무의 80% 정도는 처리됩니다:
{
"type": "object",
"properties": {
"id": { "type": "integer", "minimum": 1 },
"email": { "type": "string", "format": "email" },
"age": { "type": "integer", "minimum": 0, "maximum": 150 },
"tags": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
"role": { "enum": ["admin", "editor", "viewer"] },
"metadata": { "type": "object", "additionalProperties": true }
},
"required": ["id", "email"],
"additionalProperties": false
}
키워드 목록:
type:string,number,integer,boolean,null,array,objectproperties+required: 필드를 선언하고 필수 항목을 표시합니다enum/const: 고정 집합이나 단일 리터럴로 제한합니다minimum/maximum/multipleOf: 숫자 경계입니다minLength/maxLength/pattern: 문자열 길이와 정규식입니다minItems/maxItems/uniqueItems: 배열 형태입니다additionalProperties: false: 선언되지 않은 키를 거부합니다 (입력 계약에는 항상 설정하세요)
사용 사례별 JSON Schema 예시
위의 키워드들은 무엇을 검증하느냐에 따라 다양한 조합으로 등장합니다. 대표적인 형태 몇 가지를 살펴봅시다.
API 요청 본문 — 이메일과 비밀번호를 받는 회원가입 엔드포인트:
{
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"password": { "type": "string", "minLength": 8, "maxLength": 128 }
},
"required": ["email", "password"],
"additionalProperties": false
}
설정 파일 — 로그 레벨을 고정 집합으로 잠가 두는 로거 설정:
{
"type": "object",
"properties": {
"level": { "enum": ["debug", "info", "warn", "error"] },
"output": { "type": "string", "default": "stdout" }
},
"required": ["level"],
"additionalProperties": false
}
조건부 규칙이 있는 폼 페이로드 — accountType이 "business"일 때 taxId가 필수가 됩니다:
{
"type": "object",
"properties": {
"accountType": { "enum": ["personal", "business"] },
"taxId": { "type": "string" }
},
"if": { "properties": { "accountType": { "const": "business" } } },
"then": { "required": ["taxId"] }
}
CSV 행 JSON 레코드 — 내보내기 한 주문 테이블의 한 행:
{
"type": "object",
"properties": {
"orderId": { "type": "string", "pattern": "^ORD-[0-9]{6}$" },
"orderedOn": { "type": "string", "format": "date" },
"totalUsd": { "type": "number", "minimum": 0 }
},
"required": ["orderId", "orderedOn", "totalUsd"]
}
Webhook 이벤트 봉투 — oneOf가 type 리터럴로 분기하므로 각 이벤트 변형마다 고유한 페이로드 형태를 가집니다:
{
"oneOf": [
{ "properties": { "type": { "const": "order.created" }, "data": { "$ref": "#/$defs/order" } } },
{ "properties": { "type": { "const": "order.refunded" }, "data": { "$ref": "#/$defs/refund" } } }
]
}
이 다섯 가지 예시가 실무에서 팀들이 작성하는 스키마의 대부분을 덮습니다. 가장 가까운 형태를 가져다가 필드 이름만 손보세요. 키워드 어휘는 그대로입니다.
아무것도 설치하지 않고 검증하기
ajv.js.org나 jsonschemavalidator.net의 플레이그라운드에 스키마와 페이로드를 붙여넣으면 곧바로 결과가 나옵니다. JSON 자체가 의심스러우면 먼저 JSON 포맷터에 한 번 돌리세요.
Node.js에서 Ajv로 검증하기
설치와 12줄 예제
Ajv는 처음 compile을 호출할 때 스키마를 최적화된 함수로 컴파일하고, 이후에는 그 함수를 재사용합니다.
npm install ajv
import Ajv from "ajv";
const ajv = new Ajv();
const schema = {
type: "object",
properties: {
name: { type: "string" },
age: { type: "integer", minimum: 0 }
},
required: ["name"]
};
const validate = ajv.compile(schema);
const data = { name: "Alice", age: 30 };
if (!validate(data)) console.log(validate.errors);
else console.log("OK");
Draft 2020-12로 전환하기
기본 Ajv 생성자는 하위 호환성 때문에 아직도 Draft 7에 고정되어 있습니다. 2020-12는 명시적으로 골라야 합니다:
import Ajv2020 from "ajv/dist/2020";
const ajv = new Ajv2020({ strict: true, allErrors: true });
이제 prefixItems, unevaluatedProperties, $dynamicRef를 쓸 수 있습니다. 각 키워드가 어떤 일을 하는지는 아래 Draft 2020-12 섹션을 보세요.
format 검증 활성화
Ajv의 어떤 특이점보다 많은 개발자가 걸려 넘어지는 함정이 있습니다. format: "email"은 기본 상태에서 아무 일도 하지 않습니다. 명세상 format은 권고적(advisory)으로 취급되기 때문에, format 모듈을 직접 등록해야 합니다:
npm install ajv-formats
import addFormats from "ajv-formats";
addFormats(ajv); // 이제 "format": "email"이 실제로 검증을 수행합니다
이 단계를 빠뜨리면 format: "email"을 요구하는 스키마가 {"email": "not-an-email"}을 그냥 통과시킵니다. 프로덕션에서는 반드시 ajv-formats를 설치하세요.
Express 미들웨어 실전 적용
라우트마다 검증기 하나를, 부팅 시점에 컴파일해 둡니다:
import express from "express";
import Ajv2020 from "ajv/dist/2020";
import addFormats from "ajv-formats";
const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);
const validateUser = ajv.compile({
type: "object",
properties: {
email: { type: "string", format: "email" },
age: { type: "integer", minimum: 13 }
},
required: ["email"],
additionalProperties: false
});
const app = express();
app.use(express.json());
app.post("/users", (req, res) => {
if (!validateUser(req.body)) {
return res.status(400).json({ errors: validateUser.errors });
}
// ... 비즈니스 로직
res.status(201).json({ ok: true });
});
가장 비싸게 치르는 실수는 요청 핸들러 안에서 ajv.compile(schema)를 부르는 것입니다. 모듈 스코프에서 한 번만 컴파일하고, 반환된 함수를 계속 재사용하세요. 요청마다 다시 컴파일하면 처리량이 50배 넘게 떨어집니다.
Python에서 jsonschema로 검증하기
설치와 기본 사용법
pip install jsonschema
from jsonschema import validate, ValidationError
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name"]
}
try:
validate(instance={"name": "Alice", "age": 30}, schema=schema)
print("OK")
except ValidationError as e:
print("FAIL:", e.message, "at", list(e.absolute_path))
Draft202012Validator로 모든 오류 수집하기
validate()는 첫 오류에서 예외를 던집니다. 모든 문제를 한 번에 받아 보고 싶다면 (폼 응답에 유용합니다) iter_errors를 쓰세요:
from jsonschema import Draft202012Validator
validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(instance), key=lambda e: e.path)
for err in errors:
print(f" - {'/'.join(map(str, err.absolute_path))}: {err.message}")
이렇게 하면 사용자가 왕복(round trip)을 반복하지 않고 한 번에 모든 항목을 고칠 수 있습니다.
jsonschema vs Pydantic: 언제 무엇을 고를까
강력한 Python 라이브러리 두 개가 서로 다른 문제를 풉니다.
| 측면 | jsonschema | Pydantic v2 |
|---|---|---|
| 스키마 형식 | JSON dict (스키마 자체가 데이터) | 타입 힌트가 있는 Python 클래스 |
| 성능 | 인터프리트 방식, Pydantic보다 약 10~100배 느림 | Rust 코어, 생태계 최상위 속도 |
| 언어 간 이식성 | 가능 (같은 스키마가 JS, Go, Rust에서 동작) | 불가 (Python 전용) |
| FastAPI / 네이티브 모델 통합 | 수동 변환 | 내장 |
전체 Draft 2020-12 키워드 ($dynamicRef 등) | 완전 지원 | 부분 지원 |
프로덕션에서 통하는 규칙은 이렇습니다. 언어 간 계약(OpenAPI, 공개 API, webhook)에는 jsonschema를, 내부 Python 서비스에는 Pydantic을 쓰세요. 많은 팀이 둘을 함께 운영합니다. 게이트웨이에서는 계약 강제를 위해 jsonschema를, 애플리케이션 계층에서는 타입 안전한 비즈니스 로직을 위해 Pydantic을 씁니다. 스키마는 이식 가능한 산출물이라, Ajv에 그대로 넣어도 똑같이 동작합니다.
브라우저에서 검증하기
클라이언트 측 검증을 왜 해야 하나
중요도 순으로 세 가지 이유가 있습니다:
- UX: 사용자가 입력하는 동안 즉각 피드백을 받는 편이 서버 왕복보다 낫습니다
- 대역폭: 명백한 실수는 브라우저 밖으로 나가지 않습니다
- 보안 위생: 백엔드로 흘러드는 쓰레기 양을 줄여 줍니다 (서버 검증을 대체하지는 않습니다)
클라이언트 측 검증 하나만 믿어서는 안 됩니다. 서버에서 다시 검증하세요.
브라우저용 Ajv 번들링
npm install ajv ajv-formats
import Ajv2020 from "ajv/dist/2020";
import addFormats from "ajv-formats";
const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);
export const validateForm = ajv.compile({
type: "object",
properties: {
email: { type: "string", format: "email" },
password: { type: "string", minLength: 8 }
},
required: ["email", "password"]
});
번들에 gzip 기준 약 30 KB가 추가됩니다. 무시할 수준은 아니지만 치명적이지도 않습니다. 서버와 클라이언트가 단일 스키마 정의를 공유해야 할 때 팀들이 Ajv를 고릅니다.
더 가벼운 대안: Zod와 Valibot
JSON Schema 생태계가 필요 없고 이미 TypeScript를 쓰고 있다면, TS 네이티브 검증기 쪽이 번들도 더 가볍고 타입 추론도 더 정확합니다:
import { z } from "zod";
const UserSchema = z.object({
email: z.string().email(),
password: z.string().min(8)
});
const result = UserSchema.safeParse(data);
if (!result.success) console.log(result.error.issues);
Valibot은 비슷한 API에 gzip 기준 약 3 KB로 들어옵니다. 번들 크기가 가장 큰 변수일 때 고르세요. 다만 두 라이브러리 모두 JSON Schema를 산출하지는 않습니다. 백엔드, 서드파티 클라이언트, OpenAPI 생성기와 공유할 단일 진실 소스(source of truth)가 필요하다면 Ajv를 그대로 두세요. 전부 자체 TypeScript 안에서 끝난다면 Zod와 Valibot 쪽이 편합니다.
Draft 2020-12가 추가한 것
튜플 검증을 위한 prefixItems
Draft 7은 items: []와 additionalItems로 튜플을 표현했습니다. Draft 2020-12는 이를 깔끔하게 분리합니다:
{
"type": "array",
"prefixItems": [
{ "type": "string" },
{ "type": "number" }
],
"items": false
}
["x", 42]는 통과하고, ["x", 42, "extra"]는 실패합니다. 스키마가 하는 일이 글자 그대로 읽힙니다.
합성 스키마를 위한 unevaluatedProperties
allOf나 oneOf를 쓰는 거의 모든 팀이 한 번씩 물리는 미묘한 버그가 있습니다. additionalProperties: false는 자기 레벨에서만 검사합니다. allOf 안의 형제(sibling) 서브스키마는 마음대로 속성을 선언해도 통과해 버립니다. 2020-12의 해법은 unevaluatedProperties: false입니다:
{
"allOf": [
{ "$ref": "#/$defs/base" }
],
"unevaluatedProperties": false
}
이렇게 하면 어떤 분기에서도 평가되지 않은 속성은 전부 거부됩니다. 대다수 개발자가 additionalProperties: false에 기대했던 동작이 바로 이쪽입니다.
재귀 스키마를 위한 $dynamicRef
Draft 7에서 재귀 트리 스키마를 선언해 본 적이 있다면 그 비틀림을 잘 알 것입니다. $dynamicRef와 $dynamicAnchor가 이를 정리해 줍니다:
{
"$dynamicAnchor": "node",
"type": "object",
"properties": {
"value": { "type": "string" },
"children": { "type": "array", "items": { "$dynamicRef": "#node" } }
}
}
재귀가 선언적으로 표현되고, 자손에서 $id 재작성 없이도 재정의할 수 있습니다.
Draft 7 vs 2020-12: 어떤 것을 고를까
- 신규 프로젝트, 모던 툴체인이라면 Draft 2020-12
- OpenAPI 3.1을 만들거나 사용한다면 2020-12가 네이티브 방언입니다
- OpenAPI 3.0이나 그 이전 서비스 작업이라면 Draft 4 (OpenAPI 3.0은 Draft 4의 부분집합을 씁니다. 방언을 섞지 마세요)
- 폭넓은 검증기 호환성이 필요하다면 (Postman, 구형 CI 도구) Draft 7이 여전히 가장 안전한 교환 포맷입니다
오늘날 모던 검증기(Ajv, Python jsonschema, jsonschema-rs, Java의 networknt/json-schema-validator)는 모두 2020-12를 지원합니다.
실전 패턴
API 입력 검증
위의 Express 미들웨어가 프로덕션에 들어갈 모양입니다. 여기에 두 가지 습관을 덧붙이세요. 모든 스키마는 저장소 루트의 schemas/ 디렉토리에 모아 두고, 스키마 자체를 JSON Schema 메타 스키마에 대해 검증하는 ajv test (Python 쪽 등가물도 가능) CI 단계를 추가하세요.
설정 파일
Visual Studio Code는 SchemaStore 통합을 기본으로 갖추고 있습니다. package.json, tsconfig.json 등 수십 가지 파일에 대해 자동 완성과 인라인 검증이 동작합니다. 자체 설정 파일에 $schema 필드를 추가해 두면, 그 설정을 쓰는 에디터 사용자도 같은 혜택을 받습니다.
CI 테스트 픽스처
테스트 픽스처는 시간이 지나면 썩습니다. 누군가 모델을 업데이트했는데 픽스처는 옛 형태에 머물러 있고, 단언문이 변경된 필드를 건드리지 않아 테스트는 여전히 통과합니다. 단언문 실행 전에 스키마 검사로 이런 어긋남을 잡아내세요:
import { glob } from "glob";
const files = await glob("__tests__/fixtures/*.json");
for (const f of files) {
const data = JSON.parse(await fs.readFile(f, "utf8"));
if (!validate(data)) throw new Error(`${f}: ${ajv.errorsText(validate.errors)}`);
}
스키마 검사가 울리면 다음 수순은 보통 구조적 diff입니다. 픽스처를 JSON 비교 도구에 넣고, 최근 프로덕션 샘플과 대조해 어디가 어긋났는지 보세요. 타임스탬프와 ID가 diff를 차지해 버린다면, JSON Diff에서 타임스탬프와 ID를 무시하는 방법에 나오는 스냅샷 path-ignore 패턴을 적용해 신호와 잡음을 분리하세요.
Webhook 페이로드 (Stripe, GitHub)
서드파티 webhook은 JSON Schema의 효용이 가장 크게 드러나는 영역 중 하나입니다. webhook은 계약입니다. 제공자는 언제든 바꿀 수 있고, 우리 쪽은 바뀐 순간 알아야 합니다. Stripe와 GitHub은 OpenAPI 기술서를 공개하며, 거기서 JSON Schema를 뽑아낼 수 있습니다. 들어오는 이벤트를 검증해 두면, 호환성을 깨는 업그레이드가 조용히 상태를 망가뜨리는 대신 모니터링에 또렷하게 잡힙니다.
스키마 주도 폼 검증
React Hook Form에는 @hookform/resolvers/ajv 어댑터가 있고, Vue의 VeeValidate에도 동등한 Ajv 플러그인이 있습니다. 둘 다 하나의 JSON Schema에서 폼 렌더링, 오류 메시지, 제출 검증을 모두 끌어냅니다. 스키마가 단일 진실 소스가 되고, UI는 그 규칙을 그대로 물려받습니다.
친절한 오류 메시지
기본 메시지가 거친 이유
기본 상태에서 Ajv는 #/properties/email format must match "email" 같은 오류를 내놓습니다. 400 응답을 디버깅하는 엔지니어에게는 괜찮지만, 결제 폼을 채우는 사용자에게는 무용지물입니다.
사용자 정의 메시지를 위한 ajv-errors
npm install ajv-errors
import ajvErrors from "ajv-errors";
ajvErrors(ajv);
const schema = {
type: "object",
properties: { email: { type: "string", format: "email" } },
required: ["email"],
errorMessage: {
properties: { email: "유효한 이메일 주소를 입력하세요" },
required: { email: "이메일은 필수입니다" }
}
};
errorMessage 키워드는 스키마 안에 함께 들어 있으므로, 검증 규칙과 사용자 대상 문구가 한 덩어리로 움직입니다.
번역된 오류를 위한 ajv-i18n
ajv-i18n은 기본 메시지의 번역을 30개 이상의 언어로 갖추고 있습니다. 시작 시점에 한 줄만 추가하면 검증기가 스페인어, 프랑스어, 일본어 등 서비스하는 로케일로 말합니다. errorMessage 재정의가 모든 제약을 다 덮지 못할 때 폴백으로 쓰기 좋습니다.
스키마 경로를 폼 필드에 매핑하기
각 Ajv 오류에는 /users/0/email 같은 instancePath가 들어 있습니다. 대부분의 폼 라이브러리는 users[0].email처럼 점으로 이어진 경로를 기대하므로, 한 줄로 변환하면 됩니다:
const fieldPath = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
Python의 jsonschema에서는 같은 정보가 error.absolute_path에 들어 있습니다. .로 join하면 같은 효과를 얻을 수 있습니다.
검증은 통과하는데 프로덕션을 무너뜨리는 다섯 가지 함정
1. format은 기본적으로 권고적이다
ajv-formats 설치와 addFormats(ajv) 호출이 없으면 모든 format 키워드는 무동작(no-op)이 됩니다. {"format": "email"}은 "not-an-email"도 받아들입니다. 프로덕션에서는 반드시 format 패키지를 설치하세요.
2. additionalProperties의 기본값은 true이다
additionalProperties: false가 없으면 스키마는 선언되지 않은 모든 필드도 받아들입니다. 클라이언트는 검증을 완전히 우회하는 추가 필드를 얹어 보낼 수 있습니다. 입력 계약에서는 additionalProperties: false를 기본으로 두고, 필요할 때만 의도적으로 풀어 주세요.
3. additionalProperties는 합성되지 않는다
allOf, oneOf, anyOf 안에서 additionalProperties: false는 자기 레벨의 속성만 검사합니다. 형제 서브스키마는 빠져나갑니다. Draft 2020-12의 해법은 unevaluatedProperties: false입니다.
4. 원격 $ref는 프로덕션 위험이다
$ref: "https://example.com/schema.json"은 첫 컴파일 시 Ajv가 네트워크로 스키마를 가져오게 만듭니다. 곧 지연(latency), 원격 호스트가 멈출 때의 DoS 노출, MITM 공격 표면이 따라옵니다. 모든 $ref 대상을 인라인하거나 빌드 시점에 디스크에서 읽어 들이세요.
5. 생성된 스키마는 실제 데이터와 어긋난다
quicktype과 typescript-json-schema 같은 도구는 기존 타입에서 스키마를 만들어 줍니다. 결과물은 보통 지나치게 너그럽습니다. 모든 필드가 선택적이고, additionalProperties도 열려 있습니다. 생성된 스키마는 초안으로 보고, 손으로 조이세요. 그리고 실제 프로덕션 샘플을 스키마에 (그리고 그 반대로도) 검증하는 CI를 돌려, 어긋남(drift)이 빨리 드러나게 하세요.
성능: 수치와 어림 규칙
- Ajv (Node.js): 컴파일된 검증기는 한 건 검사를 1마이크로초도 안 되는 시간에 처리합니다. 현재 가장 빠른 프로덕션급 JS 검증기입니다.
jsonschema(Python): 인터프리트 방식이라 Pydantic보다 10~100배 느립니다. 이게 부담된다면 **fastjsonschema**로 바꾸세요. Python 코드를 생성해 Ajv에 가까운 성능을 냅니다.- Rust와 Go:
jsonschema-rs와xeipuuv/gojsonschema는 게이트웨이 계층에서 Ajv보다 2~5배 더 빠릅니다. - 가장 큰 한 가지 승리: 미리 컴파일해 두는 것입니다. 모듈 로드 시점에
ajv.compile(schema)를 한 번만 부르고, 모든 요청에서 반환된 검증기를 재사용하세요. 요청마다 다시 컴파일하면 처리량이 50배 넘게 떨어집니다.
자주 묻는 질문
JSON Schema 검증을 쉬운 말로 설명하면 무엇인가요?
JSON Schema 검증은 JSON 문서가 계약을 따르는지 확인하는 일입니다. 계약(스키마)은 그 자체가 JSON이며 타입, 필수 필드, 제약 조건을 선언합니다. 검증기는 스키마와 데이터를 읽어 “통과”라고 답하거나, 실패한 경로와 그 이유를 보고합니다.
JSON을 스키마에 대해 온라인에서 어떻게 검증하나요?
ajv.js.org 플레이그라운드나 jsonschemavalidator.net에 스키마와 데이터를 붙여넣으면 곧바로 결과가 나옵니다. JSON 자체가 이상하게 보이면 먼저 JSON 포맷터에서 정렬하세요. 두 도구 모두 브라우저 안에서 동작하며, 업로드는 발생하지 않습니다.
2026년에 가장 빠른 JSON Schema 검증기는 무엇인가요?
Node에서는 미리 컴파일된 Ajv가 한 건 검사를 1마이크로초 미만에 처리합니다. Python에서는 **fastjsonschema**가 코드를 생성해 Ajv 수준의 처리량을 냅니다. 게이트웨이 계층에서는 jsonschema-rs(Rust)와 gojsonschema(Go)가 Ajv보다 2~5배 빠릅니다. 어느 것을 골라도 한 번 컴파일해서 계속 재사용하세요.
JSON Schema와 TypeScript 타입의 차이는 무엇인가요?
TypeScript는 작성한 코드를 컴파일 타임에 검사하고, JSON Schema는 알 수 없는 JSON을 런타임에 검사합니다. TypeScript는 HTTP 응답, 파일, 사용자가 붙여넣은 JSON에는 손이 닿지 않습니다. 그 자리가 JSON Schema의 자리입니다.
Draft 2020-12를 써야 하나요, Draft 7을 써야 하나요?
2026년 신규 프로젝트라면 Draft 2020-12를 고르세요. prefixItems, unevaluatedProperties, $dynamicRef가 실제 문제를 해결합니다. OpenAPI 3.1은 2020-12를 네이티브로 씁니다. Postman 호환성이나 구형 서비스 때문일 때에만 Draft 7에 머무르세요. OpenAPI 3.0은 Draft 4의 부분집합을 쓰므로 방언을 섞지 마세요.
기존 JSON에서 JSON Schema를 어떻게 만드나요?
세 가지 길이 있습니다. quicktype.io나 jsonschema.net에 샘플을 붙여넣거나, 커맨드라인에서 npx genson-js 또는 pip install genson && genson sample.json을 돌리거나, 직접 손으로 작성하는 방법입니다. 자동 생성된 스키마는 모든 필드가 선택적이고 additionalProperties: true인 식으로 너무 너그러우니, 계약으로 쓰기 전에 항상 한 번씩 조이세요.
JSON Schema가 OpenAPI를 대체할 수 있나요?
아닙니다. OpenAPI는 요청과 응답 본문을 기술하기 위해 내부적으로 JSON Schema를 사용하고, 그 위에 경로(path), 보안 스킴, 파라미터, 서버 URL을 얹습니다. 둘은 함께 묶입니다. 스키마를 작성한 다음 OpenAPI 문서에서 참조하면 완전한 API 계약이 됩니다.
JSON Schema는 JSONPath나 jq와 같은 것인가요?
아닙니다. 다른 문제를 다룹니다. JSON Schema는 구조를 검증합니다 (“이 JSON이 계약과 일치하는가?”). JSONPath와 jq는 값을 추출합니다 (“Running 단계에 있는 모든 pod의 이름”). 검증은 스키마로, 조회는 JSONPath나 jq로 처리하세요.
Ajv 검증은 통과하는데 왜 프로덕션이 데이터를 거부하나요?
거의 모든 사례를 설명하는 세 가지 범인이 있습니다. ajv-formats를 잊어 format: "email"이 한 번도 실제로 검증되지 않은 경우, additionalProperties: false를 빠뜨려 클라이언트가 보낸 추가 필드가 빠져나간 경우, allOf나 oneOf 안에서 additionalProperties: false를 썼는데 합성되지 않는다는 사실을 뒤늦게 깨달은 경우입니다. 마지막 경우라면 unevaluatedProperties: false로 바꾸세요.
최종 사용자를 위해 JSON Schema 오류 메시지를 사용자 정의할 수 있나요?
그렇습니다. Node에서는 ajv-errors를 설치해 errorMessage를 스키마 안에 심고, ajv-i18n으로 30개 이상 로케일의 번역을 적용하세요. Python에서는 jsonschema가 각 오류 객체에 전체 검증 컨텍스트를 노출하므로, 오류 타입과 경로를 디자인 시스템이 쓰는 문구에 맞게 매핑할 수 있습니다.