쿼리와 검증 FAQ: JSONPath와 Schema
핵심 요약: 경로 쿼리에 JSONPath를 쓸지 Pointer를 쓸지, Patch와 Merge Patch를 어떻게 고를지, Schema가 무엇을 막을 수 있는지, 초안 버전을 어떻게 고르는지 한 번에 설명합니다.

핵심부터 말하면, 조건으로 노드를 찾을 때는 JSONPath, 정확한 위치를 지목하고 편집할 때는 JSON Pointer나 JSON Patch, 설정 병합에는 Merge Patch를 쓰세요. 새 Schema는 2020-12로 고정하고 allErrors를 켜 한 번에 모든 실패를 돌려받으세요.
쿼리는 "찾기", 검증은 "올바른지 확인"입니다. 각자 표준이 있으므로 섞지 마세요.
1. 경로 쿼리
JSONPath와 JSON Pointer 중 어떻게 고르나?
"조건으로 찾기"인지 "위치로 가져오기"인지에 달려 있습니다.
| 차원 | JSONPath | JSON Pointer |
|---|---|---|
| 표준 | RFC 9535(2024) | RFC 6901(2013) |
| 표현식 | $.users[?(@.age > 18)].id |
/users/0/id |
| 와일드카드와 재귀 | *, .. 지원 |
미지원 |
| 조건 필터 | 지원 | 미지원 |
| 결과 | 노드 집합(여러 개 가능) | 단일 위치 |
| 표현력 | 강하지만 구현 차이라는 무거운 유산 | 약하지만 완전히 결정적 |
| 대표 용도 | 로그 추출, 디버깅, 데이터 탐색 | JSON Patch, 설정 지목, 오류 경로 |
선택 규칙은 간단합니다:
- "조건에 맞는 모든 노드 얻기"가 필요 → JSONPath
- "정확한 위치 하나를 고정하고 추가, 삭제, 수정"이 필요 → JSON Pointer
- 함께 쓰기: JSONPath로 노드를 찾고 Pointer로 위치를 기술
API 계약의 지목에는 JSONPath를 쓰지 마세요. JSONPath는 집합을 반환하고, 빈 결과, 단일 요소, 다중 요소 세 분기가 프로토콜을 복잡하게 만듭니다. Pointer는 "존재하는 위치를 가리키거나 존재하지 않거나"라 계약에 더 맞습니다.
$..와 $.*의 차이는?
$..는 재귀 하강으로 모든 단계를 순회하고, $.*는 현재 단계의 직계 자식만 반환합니다.
{
"a": { "id": 1, "child": { "id": 2 } },
"b": { "id": 3 }
}
$.*→ 두 객체{id:1, child:{...}}와{id:3}$..id→ 세 값1,2,3$.*.id→ 중첩된2를 제외한1과3
재귀 하강은 편리하지만 비쌉니다. $..는 전체 트리를 순회해 큰 문서에서 O(n)이고, 의도하지 않은 깊은 필드를 쉽게 쓸어 담습니다.
구체적 단계가 동작하면 ..를 피하세요.
필터가 문자열을 비교할 수 있나? 정규식 매칭은?
문자열 비교는 되고, 정규식은 구현에 달려 있습니다.
표준 문법(RFC 9535):
$.items[?(@.status == "active")]
$.items[?(@.price > 10 && @.stock > 0)]
$.items[?(@.name == $.target)] // compare with another node
문자열 비교는 큰따옴표를 씁니다(RFC 9535에서는 작은따옴표도 허용되지만 일부 옛 구현은 큰따옴표만 받습니다).
숫자 비교는 숫자 의미를 따르며, "10" > 9처럼 문자열과 숫자를 섞으면 표준에서 정의되지 않은 동작이라 의존하지 마세요.
정규식은 RFC 9535가 match()와 search() 함수를 정의하지만 구현별 지원 편차가 큽니다.
안전한 접근: JSONPath가 대략 필터링하게 하고, 결과를 얻은 뒤 코드에서 정규식 매칭을 하세요.
JSON Patch를 써야 하나 Merge Patch를 써야 하나?
둘 다 "무언가를 어떻게 바꾸는가"를 기술하지만 표현력과 의미가 완전히 다릅니다.
| 차원 | JSON Patch(RFC 6902) | Merge Patch(RFC 7386) |
|---|---|---|
| 형태 | 연산 배열 [{"op":"add",...}] |
부분 객체 |
| 지원 연산 | add / remove / replace / move / copy / test | 병합만(null로 삭제) |
| 배열 요소 지목 | 가능, 인덱스로 | 불가, 배열 전체 교체 |
| 원자성 | test로 낙관적 잠금 |
없음 |
| 문서 크기 | 크지만 정확 | 작지만 표현력 제한 |
| 적합 | 세밀한 편집, API 부분 업데이트 | 설정 병합, 폼 부분 제출 |
결정 기준:
- 배열 세 번째 요소의 필드 변경 → JSON Patch
- 여러 설정 항목을 기존 설정에 병합 → Merge Patch
- "원래 값이 X가 아니면 실패" 동시성 제어 → JSON Patch의
test
Merge Patch의 null 의미 함정에 주의하세요. null은 "이 키를 삭제"이지 "값을 null로 설정"이 아닙니다.
Merge Patch로 값을 null로 설정하는 것은 불가능합니다.
2. Schema 검증
JSON Schema가 해결하는 문제는?
"데이터가 어떻게 생겨야 하는가"를 말로 된 합의에서 기계가 검사할 수 있는 계약으로 바꿉니다. 실제로 막을 수 있는 오류:
- 필수 필드 누락(
required) - 타입 오류, 예: 숫자가 와야 하는데 문자열(
type) - 범위를 벗어난 값(
minimum/maximum/minLength) - 허용 집합을 벗어난 값(
enum) - 배열 요소 타입 불일치나 잘못된 배열 길이(
items/minItems) - 중첩 구조의 필드 누락(
properties를 통한 재귀 검증) - 추가 필드(
additionalProperties: false)
해결하지 못하는 것: 업무 규칙("종료 시간이 시작 시간보다 늦어야 한다"), 필드 간 조건(if/then으로 일부 표현 가능하지만
가독성 저하), 데이터베이스 유일성, 권한 검사.
따라서 Schema는 "데이터 모양"의 문지기이지 업무 로직의 대체가 아닙니다.
draft-07인가 2020-12인가?
신규 프로젝트에는 2020-12를 쓰고, 기존 시스템은 검증기가 무엇을 지원하는지 확인하세요.
| 버전 | 상태 | 핵심 변경 |
|---|---|---|
| draft-04 | 구식 | 많은 옛 라이브러리가 여전히 기본 |
| draft-07 | 사실상 표준 | 가장 넓은 지원, 가장 풍부한 생태계 |
| 2019-09 | 과도기 | $defs, unevaluatedProperties 도입 |
| 2020-12 | 현재 권장 | items가 prefixItems + items로 분리, $dynamicRef 지원 |
이전 함정 두 가지:
- 튜플 문법 변경: draft-07에서는 배열 값
items가 튜플을 표현했고, 2020-12는 이를prefixItems로 옮겼습니다. $ref와 형제 키워드: draft-07에서는$ref가 있으면 형제 키워드가 무시되고, 2020-12는 공존을 허용합니다.
검증기가 draft-07만 지원하는데 2020-12 문법을 쓰면 조용히 무시됩니다. 가장 위험한 경우는 아무것도 검증되지 않았는데 "검증 통과"가 되는 것입니다.
Schema에서 TypeScript 타입을 어떻게 생성하나?
두 경로가 있습니다. Schema에서 타입을 도출하거나, 타입에서 Schema를 생성합니다.
Schema → 타입(백엔드가 계약을 정의하고 프런트엔드가 소비):
import type { FromSchema } from 'json-schema-to-ts';
const userSchema = {
type: 'object',
properties: {
id: { type: 'string' },
age: { type: 'integer' },
},
required: ['id'],
additionalProperties: false,
} as const;
type User = FromSchema<typeof userSchema>;
// { id: string; age?: number }
핵심은 as const입니다. 없으면 type: 'string'이 리터럴 'string'이 아니라 string으로 추론되어 추론이 무너집니다.
타입 → Schema(프런트엔드에 이미 타입이 있고 런타임 검증이 필요):
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const userSchema = z.object({ id: z.string(), age: z.number().int().optional() });
const jsonSchema = zodToJsonSchema(userSchema);
이 경로의 장점은 "한 정의에서 타입과 런타임 검증을 모두 얻는다"는 것입니다. 비용은 DSL 도입이고,
생성된 Schema가 구현 디테일(예: 보기 좋지 않은 $ref 구조)을 담을 수 있습니다.
검증 실패 시 특정 필드를 어떻게 찾나?
검증기가 반환하는 instancePath(2020-12)나 dataPath(draft-07)를 보세요. 오류 위치를 가리키는 JSON Pointer입니다.
[
{
"instancePath": "/items/2/price",
"schemaPath": "#/properties/items/items/properties/price/type",
"keyword": "type",
"message": "must be number"
}
]
instancePath: "/items/2/price"는 직접 알려 줍니다. items 배열의 세 번째 요소 price 필드 타입이 틀렸습니다.
실용 제안 두 가지:
- 첫 오류에서 던지지 말고 모든 오류를 한 번에 반환: 프런트엔드가 한 번에 모두 고치는 것이 오가며 시도하는 것보다 빠릅니다.
- Pointer를 최종 사용자에게 보여 주기 전에 평문으로 변환:
/items/2/price는 개발자에게 유용하고 사용자에게는 무용합니다.
allErrors: true가 대부분 검증기의 스위치입니다(Ajv는 명시적으로 켜야 함). 기본은 흔히 fail-fast 모드입니다.
additionalProperties: false를 켜야 하나?
내부 계약에는 켜고, 공개 API에는 신중하세요.
켜면 properties 아래 선언되지 않은 필드는 검증에 실패합니다. 이점은 오타(userNmae)와 예상 밖 통과 포착이고,
단점은 업스트림 필드 추가가 다운스트림 소비자를 깨뜨려 역호환성을 파괴한다는 것입니다.
절충:
| 시나리오 | 권장 |
|---|---|
| 내부 서비스 간 | 켜기. 필드 변경은 버전 관리를 거침 |
| 공개 외부 API | 끄기. 업스트림 필드 추가 허용 |
| 서드파티 데이터 소비 | 끄기. 상대를 통제할 수 없음 |
| 설정 검증 | 켜기. 설정 오타는 바로 실패해야 함 |
배열 요소가 유일한지 어떻게 검증하나?
두 다른 의미이므로 혼동하지 마세요:
{ "uniqueItems": true }
전체 요소를 중복 제거하며, 배열에 깊이 동등한 두 요소가 없어야 합니다.
{
"type": "array",
"items": { "type": "object", "properties": { "id": { "type": "string" } } },
"uniqueItems": true
}
위 조각은 객체 전체가 중복되지 않음만 보장합니다. {"id":"a","name":"x"}와 {"id":"a","name":"y"}는 중복으로 세지 않습니다.
특정 필드로 중복 제거는 Schema가 표현하지 못하고 업무 계층에서 검증해야 합니다:
const ids = items.map((item) => item.id);
if (new Set(ids).size !== ids.length) {
throw new Error('id 重复');
}
format이 실제로 이메일과 날짜를 검증하나?
반드시 그렇지 않습니다. 명세에서 format은 기본적으로 "단언"이 아니라 "주석"이라 검증기가 수집만 하고 검증하지 않을 수 있습니다.
- Ajv는 별도
ajv-formats패키지를 설치하고 구성해야 하며, 아니면format: "email"이 사실상 무력합니다. - 일부 검증기는
format을 기본 활성 단언으로 취급해 동작이 일관되지 않습니다. - 활성화해도
email정규식이 매우 느슨해a@b도 통과합니다.
결론: format을 강한 검증으로 취급하지 마세요. 이메일, URL, 날짜의 엄격 검증은 업무 코드에서 하세요.
Schema에 format을 문서로 남기는 값어치가 있지만 더러운 데이터를 막아 줄 거라 기대하지 마세요.
3. 빠른 참조
| 질문 | 결론 |
|---|---|
| 조건으로 노드 찾기 | JSONPath |
| 정확한 지목, 패치 적용 | JSON Pointer |
| 배열 요소 변경 | JSON Patch, Merge Patch는 못 함 |
| 설정 병합 | Merge Patch |
| 신규 프로젝트 Schema 버전 | 2020-12, prefixItems 주의 |
| Schema에서 TS 타입 생성 | json-schema-to-ts + as const |
| 검증 오류 찾기 | instancePath 읽고 allErrors 켜기 |
| 필드로 중복 제거 | Schema는 못 함, 업무 계층에서 Set |
format: email |
대부분 검증기가 기본으로 검사하지 않으므로 의존하지 말 것 |