JSON Schema로 API 페이로드 검증하기
JSON Schema가 손으로 쓴 페이로드 검증을 선언적 계약으로 어떻게 대체하는지, 중요한 키워드들, 그리고 사람들을 무는 format 함정을 정리합니다.
손으로 쓴 검증은 썩습니다. if (!body.email) return 400으로 시작해, 길이 검사를
더하고, 정규식을 더하고, 지난 분기에 누군가 더한 선택 필드를 위한 분기를 더합니다.
검사는 문서에서 표류하고, 에러 메시지는 일관되지 않고, 아무도 생각하지 못한
경우(age: -1, role: "suuper-admin", 클라이언트가 결코 보내면 안 되는 떠도는
isAdmin: true)가 곧장 통과합니다. JSON Schema는 그 전부를 선언적 계약으로
대체합니다. 다른 JSON 문서가 취해야 할 모양을 기술하는 JSON 문서이고, 당신이
손으로 유지하는 코드가 아니라 검증기가 검사합니다.
작업 스키마
사용자 생성 페이로드용 스키마입니다.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"username": { "type": "string", "minLength": 3, "pattern": "^[a-z0-9_]+$" },
"age": { "type": "integer", "minimum": 13 },
"role": { "type": "string", "enum": ["reader", "author", "admin"] },
"tags": {
"type": "array",
"items": { "type": "string" },
"maxItems": 10
}
},
"required": ["email", "username", "role"],
"additionalProperties": false
}
통과하는 인스턴스입니다.
{
"email": "[email protected]",
"username": "lin_99",
"age": 27,
"role": "author",
"tags": ["ml", "rust"]
}
실패하는 인스턴스입니다.
{
"email": "[email protected]",
"username": "Li",
"age": 11,
"role": "superuser",
"isAdmin": true
}
검증기는 위반된 키워드마다 에러 하나를 내보내며, 각각이 인스턴스의 정확한 위치를 가리킵니다. 모양은 라이브러리마다 다르지만, 실질은 일관됩니다.
/username : "Li"는 너무 짧음 (minLength 3)
/username : "Li"가 패턴 "^[a-z0-9_]+$"와 맞지 않음
/age : 11이 최소 13보다 작음
/role : "superuser"가 ["reader","author","admin"] 중 하나가 아님
/ : 추가 속성 "isAdmin"이 허용되지 않음
그것은 하나의 선언적 문서에서 나온 다섯 개의 구별되는, 위치 지정된 에러입니다. 동등한 손 작성 코드는 당신이 문서의 스키마와 영원히 동기화해야 하는 수십 줄입니다.
키워드, 묶어서
JSON Schema는 키워드가 많지만, 셋의 일로 떨어집니다.
구조는 모양을 기술합니다. type이 JSON 타입(object, array, string,
number, integer, boolean, null)을 제약합니다. properties가 키를 하위
스키마에 매핑합니다. required가 있어야 하는 키를 나열합니다. 그것이 비널을
함의하지 않고 존재만 함의함에 주의하세요. items가 배열의 모든 요소에 하위
스키마를 적용합니다. additionalProperties가 properties 밖 키가 허용되는지
통제합니다.
값 제약은 개별 값을 좁힙니다. minimum/maximum(그리고 배타 변형)이 숫자를
묶고, minLength/maxLength가 문자열을, minItems/maxItems가 배열을 묶습니다.
pattern이 문자열에 정규식을 적용합니다. enum이 값을 고정 목록으로 제한하고,
const가 정확히 한 값으로 고정하는데, 이는 "type": { "const": "user.created" }
같은 판별자 필드에 편리합니다.
구성은 하위 스키마를 결합합니다. allOf가 모든 가지의 매칭을, anyOf가
적어도 하나를, oneOf가 정확히 하나를 요구합니다. $ref가 이름 붙은 하위
스키마를 가리켜 모양을 한 번 정의해 재사용하게 하고, $defs가 그 정의를 두는
관례적 자리입니다. 재사용 가능한 주소 블록은 이렇게 생겼습니다.
{
"type": "object",
"properties": {
"billing": { "$ref": "#/$defs/address" },
"shipping": { "$ref": "#/$defs/address" }
},
"$defs": {
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"country": { "type": "string", "pattern": "^[A-Z]{2}$" }
},
"required": ["street", "country"]
}
}
}
oneOf는 태그된 유니온의 보통 도구입니다. 이메일 이벤트이거나 SMS 이벤트인
페이로드를 const 채널 필드로 구별하는 것입니다. 하지만 oneOf가 정확히
하나의 매칭을 강제하므로, 겹치는 가지는 헷갈리는 "하나보다 많이 매칭됨" 에러를
낳습니다. 가지가 판별자에서 진짜로 상호 배타적이면 oneOf가 맞고, 단지 겹치면
보통 anyOf가 의도한 것입니다.
format 함정
format은 사람들이 가장 자주 오독하는 키워드입니다. "format": "email"을 쓰면
잘못된 주소를 거부할 것처럼 보이고, "format": "date-time"이 나쁜 타임스탬프를
거부할 것처럼 보입니다. 많은 검증기에서, 기본적으로, 그러지 않습니다. format은
주석, 즉 의도된 의미론에 대한 힌트이고, 단언은 명시적으로 켜지 않는 한 꺼져
있습니다.
그래서 "format": "email"을 가진 스키마가 "not an email"을 기꺼이 받아들일 수
있습니다. 검증기가 format을 메타데이터로 기록하고 넘어가기 때문입니다. format이
실제로 값을 제약하게 하려면 format 단언을 켜야 합니다. Ajv의 생성자
플래그(validateFormats), 2020-12의 format 어휘, 또는 당신 언어 라이브러리의 동등
옵션입니다. 동작은 구현과 드래프트마다 다르므로, 안전한 가정은 "format은 내
검증기에서 작동함을 증명하기 전까지 아무것도 하지 않는다"입니다. 보장이 필요하면
format을 항상 단언되는 pattern으로 받치세요.
additionalProperties로 엄격하게
기본적으로 JSON Schema는 당신이 언급하지 않은 키를 무시합니다. 예상치 못한
isAdmin: true를 나르는 페이로드가, 당신이 달리 말하지 않는 한 잘 검증됩니다.
"additionalProperties": false를 설정하면 properties에 이름 붙지 않은 어떤 키든
거부하고, 이것이 스키마를 닫힌 계약으로 바꾸고 usrename 같은 오타나 클라이언트가
결코 보내면 안 되는 필드를 잡습니다.
트레이드오프는 전방 호환성입니다. 엄격한 스키마는 미래 클라이언트 버전이 더할 수 있는 추가 필드를 거부하므로, v2 클라이언트가 v1 서버와 말할 때 서버가 안전하게 무시했을 속성에서 깨집니다. 양 끝을 통제하는 내부 API에는 엄격이 올바른 기본값입니다. 알 수 없는 키는 버그입니다. 서버보다 앞선 클라이언트를 견뎌야 하는 공개 API에는 열어 두는 것(또는 당신이 진짜로 소유하는 필드로 엄격성을 한정하는 것)이 양성 추가에 깨지지 않게 합니다.
값을 하는 곳
선언적 형태는 같은 모양이 한 곳보다 많이 기술되는 어디서나 값을 합니다.
- API 경계. 핸들러가 돌기 전에 요청을, 출하 전에 응답을 검증하세요. 스키마가 단일 진실 소스가 되고, 당신이 반환하는 400이 손 조립이 아니라 거기서 생성됩니다.
- 설정 파일. 서비스 설정 위 스키마가 잘못 쓴 키나 범위 밖 타임아웃을 새벽 3시가 아니라 시작 시 잡습니다. 이것은 설정이 JSON이든 YAML이든 적용됩니다. 같은 데이터 모델을 기술하기 때문입니다. 둘이 어디서 갈라지는지는 JSON vs YAML을 보세요.
- OpenAPI. OpenAPI는 요청·응답 본문에 JSON Schema를 포함하므로, 당신이 쓰는 스키마가 API 명세로도 두 배 역할을 합니다.
- 코드 생성. 많은 툴체인이 스키마에서 직접 타입이나 클라이언트 코드를 생성해, 모델과 코드를 발맞춰 유지합니다.
- 편집기 지원. 편집기를 스키마에 가리키면 설정을 손 편집하는 동안 인라인
검증과 자동완성을 얻습니다.
tsconfig.json과package.json을 자가 검사하게 만드는 같은 경험입니다.
드래프트와 당신의 검증기가 지원하는 것
JSON Schema는 드래프트로 버전 매겨집니다. 2020-12가 최신이고 새 작업에 겨냥할
것입니다. draft-07이 여전히 극히 흔한데, 많은 도구가 거기 정착해 결코 옮기지
않았기 때문입니다. draft-04가 옛 OpenAPI 2.0 스택에 여전히 나타납니다. 위
키워드는 최근 드래프트에 걸쳐 안정적이지만, 세부, 즉 $ref가 어떻게 해석되는지,
format이 단언하는지, $defs 대 옛 definitions의 철자는 버전 사이에
바뀌었습니다. 문서 맨 위 $schema 선언이 드래프트를 명명합니다. 그것을 존중하고,
당신의 검증기가 가정이 아니라 실제로 그 드래프트를 구현하는지 확인하세요. 여기
불일치는 당신이 생각하는 것보다 덜 검증하는 스키마를 조용히 낳습니다.
스키마를 먼저 쓰고, 실제 페이로드에 대해 일찍 검증하고, 계약이 그것이 지키는 코드 옆에 살게 하세요. 브라우저에서 스키마를 샘플 문서에 대해 초안 잡고 테스트하려면, 우리 JSON Schema 검증기가 인스턴스를 스키마에 대해 검사하고 각 에러를 그 위치와 함께 보고합니다. 그리고 붙여 넣기 전에 페이로드를 정리하기만 하면 된다면, JSON 포매터가 먼저 예쁘게 출력하고 린트합니다.