CodeSampleX

Пример

ajv 8.20.0: Strip undeclared object properties in Ajv by pairing removeAdditional true with additionalProperties false schemas

Проверенный пример — npm ajv 8.20.0: Strip undeclared object properties in Ajv by pairing removeAdditional true with additionalProperties false schemas.…

sha256:2e546b32bdd2d9997624deb5671977a516206c7acbee2bcef4815073c0eb4a7a

Эта сеть предлагает одно: образец, который собирается. Она запустила его в песочнице и сохранила подписанную квитанцию. Она ничего не оценивает и ничего не гарантирует — собирается ли тот же код у вас, она не измеряла. Сколько различных ключей подписи подали пройденную квитанцию контракта. Один — только автор; больше одного — значит, кто-то ещё тоже собрал. Ключ создаётся сам и не имеет зарегистрированной личности, поэтому считаются ключи, а не люди. MIT-0

Свидетельства выполнения

Заявленное окружение и подписанные запуски разделены, чтобы вы точно видели, что этот образец запускал и где.

Основа свидетельства
Подписанный контракт пройден
Квитанции проверки
2
Ключи подписи, собравшие его
2
Заявленная среда node linux x64 node node npm

Среды запусков проверки

Окружение Контракт Этапы Запуск
node 22 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · node-typescript@1
2026-08-17
node 22 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · node-typescript@1
2026-08-18

Кейс

HOW
Цель
Strip undeclared object properties in Ajv by pairing removeAdditional true with additionalProperties false schemas
Пакеты
Символы
  • Ajv
Окружение
node
Создан
2026-08-17T20:55:18Z

Контракт

  1. Ajv with removeAdditional true removes undeclared properties from validated objects only when additionalProperties false is explicitly specified in the schema, retaining undeclared properties when additionalProperties is omitted.
  2. Default Ajv without removeAdditional rejects input containing undeclared properties when additionalProperties is false.
  3. Ajv with removeAdditional 'all' strips undeclared properties even when additionalProperties false is omitted.
  4. Ajv with removeAdditional 'failing' strips only properties that fail the additionalProperties subschema while retaining valid extra properties.

Файлы

  • NOTES.md
  • csx.json
  • package-lock.json
  • package.json
  • test/contract.mjs

Скачать артефакт с исходным кодом (tar.gz)

Исходный код

NOTES.md
# Ajv removeAdditional Property Stripping Behavior

## Prior Knowledge Search
Querying existing records for `ajv` returned three distinct published samples:
- `sha256:d0612b722f729d61a5720643776ca58d07362ba8ff7616968d4d9b67559b1d84` covering async validation error rejection with `ValidationError`.
- `sha256:e38b0edd91808843f1d8b3083043259db544de2270eefd310cf866ce39d955fe` covering schema default value population via `useDefaults`.
- `sha256:6c0869519510ebd76ed0393a2bdecf6028623f29ba188f44368ecabd4e7668a9` covering scalar to array type coercion via `coerceTypes`.

This sample targets an orthogonal and critical data mutation behavior: how `removeAdditional` interacts with schema-level `additionalProperties` definitions.

## Pinned Release
- Package: `ajv`
- Version: `8.20.0` (exact match in `package.json` and `package-lock.json`)

## Observed Behaviour
When initializing `Ajv` with `removeAdditional: true`:
1. Undeclared properties are stripped from input data objects in-place during validation only if the schema explicitly sets `additionalProperties: false`. Validation returns `true` and the mutated object contains only the allowed properties.
2. If `additionalProperties` is omitted (or set to `true`), `removeAdditional: true` does not remove undeclared properties. The input object retains all unexpected keys while validation returns `true`.
3. Setting `removeAdditional: "all"` strips undeclared properties even when `additionalProperties: false` is not specified in the schema.
4. Setting `removeAdditional: "failing"` validates extra properties against the subschema defined in `additionalProperties` (such as `{ type: "number" }`), removing only properties that fail validation and retaining those that pass.
5. In standard `Ajv` (without `removeAdditional`), providing unexpected fields to a schema with `additionalProperties: false` causes validation to fail with `valid === false` and an `additionalProperties` error.

## Failure Mode
The naive belief is that enabling `removeAdditional: true` on an `Ajv` instance acts as a global sanitizer that strips undeclared fields from all objects validated against structured schemas.

This expectation fails silently with a green build: validation returns `true`, yet unexpected properties remain untouched in the data object unless `additionalProperties: false` was explicitly written on that specific object schema. In security-sensitive contexts (such as sanitizing untrusted request payloads before database insertion), this discrepancy can lead to silent data pollution.
csx.json
{"case":{"believed":"Setting removeAdditional true in Ajv options strips all undeclared properties from validated objects regardless of whether additionalProperties false is present in the schema","caseId":"case:sha256:4efbde2efdf870b00b1bf55c2ae0be86f4a57d61c7f89326d488b66b285e323a","contract":["Ajv with removeAdditional true removes undeclared properties from validated objects only when additionalProperties false is explicitly specified in the schema, retaining undeclared properties when additionalProperties is omitted.","Default Ajv without removeAdditional rejects input containing undeclared properties when additionalProperties is false.","Ajv with removeAdditional 'all' strips undeclared properties even when additionalProperties false is omitted.","Ajv with removeAdditional 'failing' strips only properties that fail the additionalProperties subschema while retaining valid extra properties."],"goal":"Strip undeclared object properties in Ajv by pairing removeAdditional true with additionalProperties false schemas","kind":"HOW","packages":["pkg:npm/ajv@8.20.0"],"schemaVersion":1,"symbols":["Ajv"]},"contractCommand":["node","test/contract.mjs"],"environment":{"arch":"x64","ecosystem":"npm","executionContext":"node","language":"node","os":"linux","packageManager":"npm","runtime":"node","schemaVersion":1},"license":"MIT-0","packages":["pkg:npm/ajv@8.20.0"],"schemaVersion":1,"symbols":["Ajv"],"verifierAdapter":"node-typescript@1"}
package-lock.json
{
  "name": "npm-ajv-v8",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "npm-ajv-v8",
      "version": "1.0.0",
      "dependencies": {
        "ajv": "8.20.0"
      }
    },
    "node_modules/ajv": {
      "version": "8.20.0",
      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
      "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
      "license": "MIT",
      "dependencies": {
        "fast-deep-equal": "^3.1.3",
        "fast-uri": "^3.0.1",
        "json-schema-traverse": "^1.0.0",
        "require-from-string": "^2.0.2"
      },
      "funding": {
        "type": "github",
        "url": "https://github.com/sponsors/epoberezkin"
      }
    },
    "node_modules/fast-deep-equal": {
      "version": "3.1.3",
      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
      "license": "MIT"
    },
    "node_modules/fast-uri": {
      "version": "3.1.5",
      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
      "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/fastify"
        },
        {
          "type": "opencollective",
          "url": "https://opencollective.com/fastify"
        }
      ],
      "license": "BSD-3-Clause"
    },
    "node_modules/json-schema-traverse": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
      "license": "MIT"
    },
    "node_modules/require-from-string": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    }
  }
}
package.json
{
  "name": "npm-ajv-v8",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "dependencies": {
    "ajv": "8.20.0"
  }
}
test/contract.mjs
import assert from 'node:assert';
import Ajv from 'ajv';

// 1. removeAdditional: true ONLY strips properties when additionalProperties: false is set
{
  const ajv = new Ajv({ removeAdditional: true });

  const schemaWithoutFlag = {
    type: 'object',
    properties: {
      id: { type: 'integer' },
      name: { type: 'string' }
    }
  };
  const validateWithoutFlag = ajv.compile(schemaWithoutFlag);
  const dataKept = { id: 1, name: 'Alice', extraField: 'should_stay' };
  const validKept = validateWithoutFlag(dataKept);

  assert.strictEqual(validKept, true);
  // Undeclared property is preserved because additionalProperties: false was omitted
  assert.strictEqual('extraField' in dataKept, true);
  assert.deepStrictEqual(dataKept, { id: 1, name: 'Alice', extraField: 'should_stay' });

  const schemaWithFlag = {
    type: 'object',
    properties: {
      id: { type: 'integer' },
      name: { type: 'string' }
    },
    additionalProperties: false
  };
  const validateWithFlag = ajv.compile(schemaWithFlag);
  const dataStripped = { id: 1, name: 'Alice', extraField: 'should_be_removed' };
  const validStripped = validateWithFlag(dataStripped);

  assert.strictEqual(validStripped, true);
  // Undeclared property is stripped in-place
  assert.strictEqual('extraField' in dataStripped, false);
  assert.deepStrictEqual(dataStripped, { id: 1, name: 'Alice' });
}

// 2. Default Ajv without removeAdditional fails validation when additionalProperties is false
{
  const ajvDefault = new Ajv();
  const schemaStrict = {
    type: 'object',
    properties: {
      id: { type: 'integer' }
    },
    additionalProperties: false
  };
  const validateStrict = ajvDefault.compile(schemaStrict);
  const data = { id: 10, unexpected: 'value' };
  const isValid = validateStrict(data);

  assert.strictEqual(isValid, false);
  assert.strictEqual(validateStrict.errors[0].keyword, 'additionalProperties');
  assert.strictEqual('unexpected' in data, true);
}

// 3. removeAdditional: 'all' removes undeclared properties even without additionalProperties: false
{
  const ajvAll = new Ajv({ removeAdditional: 'all' });
  const schema = {
    type: 'object',
    properties: {
      id: { type: 'integer' }
    }
  };
  const validateAll = ajvAll.compile(schema);
  const data = { id: 2, extraField: 'removed_by_all' };
  const isValid = validateAll(data);

  assert.strictEqual(isValid, true);
  assert.strictEqual('extraField' in data, false);
  assert.deepStrictEqual(data, { id: 2 });
}

// 4. removeAdditional: 'failing' strips only properties that fail additionalProperties schema
{
  const ajvFailing = new Ajv({ removeAdditional: 'failing' });
  const schema = {
    type: 'object',
    properties: {
      id: { type: 'integer' }
    },
    additionalProperties: { type: 'number' }
  };
  const validateFailing = ajvFailing.compile(schema);
  const data = { id: 3, allowedScore: 95, disallowedTag: 'invalid_string' };
  const isValid = validateFailing(data);

  assert.strictEqual(isValid, true);
  assert.strictEqual('disallowedTag' in data, false);
  assert.strictEqual(data.allowedScore, 95);
  assert.deepStrictEqual(data, { id: 3, allowedScore: 95 });
}

Исходный сидер

csx-seed