JSON Toolkit

additionalProperties를 닫지 않으면 Schema 검증은 검증이 아니다

핵심 요약: 검증을 통과해도 데이터가 깨끗하다는 뜻이 아닙니다. Pydantic은 기본으로 미선언 필드를 무시하고, Zod는 제거하며, Ajv는 그대로 통과시킵니다. 본문은 무음 수용의 세 가지 형태를 보이고, 객체를 닫고 강제 형변환을 제어하며 모든 중첩 단계를 조이는 완전한 방법을 제시합니다.

Intermediate약 8분 소요2026-11-02 발행2026-11-02 업데이트
불필요한 필드가 포함된 JSON이 검증을 통과하는 모습

결론: 기본값에서 주요 검증기는 미선언 필드를 "거부"가 아니라 "무시"합니다. additionalProperties: false를 쓰지 않으면 검증 통과는 "선언된 필드에 문제가 없다"는 것만 보증합니다.

검증기의 가장 위험한 동작은 에러를 내는 것이 아니라 에러를 내지 않는 것입니다. 오타 키와 모델이 지어낸 인자는 검증층을 조용히 통과해 비즈니스층에서 터집니다.

一: "통과"하는 검증의 실례

도구 호출 인자 계약에 두 필드만 선언했다고 합시다:

{
  "type": "object",
  "properties": {
    "order_id": { "type": "string" },
    "amount": { "type": "number" }
  },
  "required": ["order_id", "amount"]
}

{"order_id": "A1", "amount": 50, "reasn_code": "damaged"}를 넣으면——세 번째 키는 reason_code의 오타입니다. Ajv 기본 설정으로 검증하면 결과는 통과입니다. 여분의 reasn_code는 거부도 제거도 되지 않고 그대로 비즈니스 로직에 들어갑니다.

JSON Schema 규격에서 additionalProperties의 기본값은 true입니다. "어떤 필드가 있는가"는 선언했지만 "그 필드만 있어야 한다"는 선언이 없으니 검증기는 규격대로 동작한 것입니다.

二: 런타임의 세 가지 "무음"

런타임 미선언 키의 기본 동작 결과
Ajv (JS, 기본) 그대로 통과 오염 데이터가 비즈니스층으로
Zod z.object (TS) 제거(strip) 필드가 사라지고 하류 객체가 짧아짐
Pydantic v2 (Python) 무시(extra="ignore") 동일하며 로그에 흔적 없음

제거는 통과보다 더 은밀합니다. dev.to의 비교 글은 이렇게 요약합니다: 두 라이브러리 모두 기본 상태에서 똑같이——조용히——실패합니다.

三: LLM 도구 호출이 문제를 확대한다

LLM 도구 호출 인자는 모델이 생성합니다. 필드명을 지어내고, "3"을 숫자로 전달하고, 필수 필드에 모호한 값을 넣습니다. 플랫폼은 인자를 JSON Schema로 정의하지만 기본으로 schema를 닫도록 강제하지 않습니다——strict 모드는 명시적으로 켜야 하고 문법 제약은 모델 직접 출력에만 적용됩니다.

방어선은 로컬에 세워야 합니다:

{
  "order_id": 4411,
  "refund_amount_usd": 50,
  "reason_code": "damaged",
  "urgent": "yes"
}

객체를 닫지 않으면 뒤의 두 키가 그대로 통과하고, 닫으면 reason_code와 urgent가 거부되며 에러가 /reason_code 경로를 가리킵니다.

四: 닫는 방법: 세 생태계 비교

Ajv: 객체 노드에 additionalProperties: false 추가.

import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true });

const schema = {
  type: "object",
  properties: { order_id: { type: "string" }, amount: { type: "number" } },
  required: ["order_id", "amount"],
  additionalProperties: false,
};

const validate = ajv.compile(schema);
validate({ order_id: "A1", amount: 50, reasn_code: "x" }); // false
validate.errors[0].instancePath; // "/reasn_code"

Zod: z.object를 z.strictObject로 교체하거나 .strict() 체인. Pydantic: ConfigDict(extra="forbid") 설정.

공통 규칙은 단계별로 닫기입니다. 중첩된 각 객체에도 각각 additionalProperties: false가 필요하며, 한 단계라도 빠지면 그 단계가 새 구멍이 됩니다.

五: 강제 형변환 정책도 정하기

Pydantic lax 모드는 "5"를 5로 바꿉니다. 정책은 세 가지:

  1. 엄격 모드: 타입이 틀리면 거부 (Pydantic strict=True, coerce 없는 Zod, Ajv는 애초에 강제 변환 없음);
  2. 제어된 변환: 명시적 변환만 허용 (Zod z.coerce.number()) + 범위 .positive().max(10000);
  3. 완전 허용: 내부의 알려진 데이터 소스에만.

LLM 인자 같은 신뢰할 수 없는 입력에는 1 또는 2를 고르고 테스트에 명시합니다. 의미가 모호한 필드(단위 없는 금액 등)는 refund_amount_usd처럼 단위를 필드명에 넣습니다.

六: 중첩과 배열 체크리스트

점검점 방법
최상위 객체 additionalProperties: false
각 중첩 object 동일하게 닫기
배열 요소 items 안의 객체도 닫기
열거와 범위 enum, minimum/maximum 보강
생성기 출력 키워드 수동 보완

properties 선언은 required 선언이 아닙니다. 생성기 산출물에 대해서는《자동 생성된 JSON Schema가 왜 실제 데이터를 거부하는가》를 참고하세요.

七: 거부 후 에러를 필드로 연결

allErrors: true로 모든 위반 필드를 한 번에 보고하고, verbose: true로 구체 경로를 얻습니다. instancePath 처리는《JSON Schema 에러 메시지 이해하기》를 참고하세요. 구문 수준 문제는 schema 검증 전에 /parse에서 걸러집니다.

자주 묻는 질문

additionalProperties: false는 너무 엄격한가요?

계약에 달렸습니다. 스스로 정의하는 입력이라면 닫아야 하고, 확장 여지가 있는 서드파티 데이터는 핵심만 닫고 확장 영역을 명확히 나눕니다.

Zod strip이 더 안전하지 않나요?

strip은 하류를 보호하고 호출자를 해칩니다. 에러는 빨리 명시적으로 나타나야 하며 strip은 그 반대입니다.

strict 모드가 있으면 로컬 검증이 불필요한가요?

필요합니다. schema 자체가 열려 있을 수 있고 문법 제약이 도구 인자를 커버하지 않을 수 있으므로 로컬 재검증 비용은 거의 없습니다.

닫은 후 기존 데이터가 대량 실패하면?

그것이 노출해야 할 문제입니다. allErrors로 목록화하고 무해한 키는 properties에 추가, 유해한 것은 데이터 수정. 통과시키려고 스위치를 되돌리지 마세요.

한 줄 해결책이 있나요?

없지만 최소 동작은: 모든 schema 정의를 검색해 각 type: "object" 뒤에 additionalProperties: false가 있는지 확인하는 것입니다.