From 7007cfc385308fa8e3087508aae7374062b32410 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 13 Oct 2026 17:13:14 +0000 Subject: [PATCH] fix: scope largeArrayMechism options per build() call largeArraySize and largeArrayMechanism were module-level state that build() mutated and never reset, so an unrelated build() call opting into 'json-stringify' for its own schema leaked into every serializer built afterwards, even ones built with no options. This silently defeated the documented schema filtering (missing fields / additionalProperties: false). Move both values onto the per-build context and branch on them there. Add a regression test that builds a serializer without options after a json-stringify build and asserts filtering is preserved. --- index.js | 23 +++++++++++------------ test/array.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 63372eaa..07bf545d 100644 --- a/index.js +++ b/index.js @@ -10,9 +10,6 @@ const Location = require('./lib/location') const validate = require('./lib/schema-validator') const mergeSchemas = require('./lib/merge-schemas') -let largeArraySize = 2e4 -let largeArrayMechanism = 'default' - const DEFAULT_MAX_DEPTH = 100 const NAMED_FRAGMENT_REF = /^#[a-z_][-\w._]*$/i @@ -345,6 +342,8 @@ function build (schema, options) { recursiveSchemas: new Set(), recursivePaths: new Set(), buildingSet: new Set(), + largeArraySize: 2e4, + largeArrayMechanism: 'default', uid: 0 } @@ -374,7 +373,7 @@ function build (schema, options) { if (options.largeArrayMechanism) { if (validLargeArrayMechanisms.has(options.largeArrayMechanism)) { - largeArrayMechanism = options.largeArrayMechanism + context.largeArrayMechanism = options.largeArrayMechanism } else { throw new Error(`Unsupported large array mechanism ${options.largeArrayMechanism}`) } @@ -385,11 +384,11 @@ function build (schema, options) { let parsedNumber if (largeArraySizeType === 'string' && Number.isFinite((parsedNumber = Number.parseInt(options.largeArraySize, 10)))) { - largeArraySize = parsedNumber + context.largeArraySize = parsedNumber } else if (largeArraySizeType === 'number' && Number.isInteger(options.largeArraySize)) { - largeArraySize = options.largeArraySize + context.largeArraySize = options.largeArraySize } else if (largeArraySizeType === 'bigint') { - largeArraySize = Number(options.largeArraySize) + context.largeArraySize = Number(options.largeArraySize) } else { throw new Error(`Unsupported large array size. Expected integer-like, got ${typeof options.largeArraySize} with value ${options.largeArraySize}`) } @@ -895,8 +894,8 @@ function buildArray (context, location, input) { ` } - if (largeArrayMechanism === 'json-stringify') { - functionCode += `if (arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n` + if (context.largeArrayMechanism === 'json-stringify') { + functionCode += `if (arrayLength >= ${context.largeArraySize}) return JSON.stringify(obj)\n` } functionCode += ` @@ -980,8 +979,8 @@ function buildArray (context, location, input) { ` } - if (largeArrayMechanism === 'json-stringify') { - inlinedCode += `if (arrayLength_${objVar} >= ${largeArraySize}) json += JSON.stringify(${objVar})\n else {` + if (context.largeArrayMechanism === 'json-stringify') { + inlinedCode += `if (arrayLength_${objVar} >= ${context.largeArraySize}) json += JSON.stringify(${objVar})\n else {` } inlinedCode += ` @@ -1038,7 +1037,7 @@ function buildArray (context, location, input) { json += JSON_STR_END_ARRAY ` - if (largeArrayMechanism === 'json-stringify') { + if (context.largeArrayMechanism === 'json-stringify') { inlinedCode += '}' } diff --git a/test/array.test.js b/test/array.test.js index 41d8fcb0..ded80238 100644 --- a/test/array.test.js +++ b/test/array.test.js @@ -23,6 +23,31 @@ test('error on invalid largeArrayMechanism', (t) => { }), Error('Unsupported large array mechanism invalid')) }) +test('does not leak largeArrayMechanism across build() calls', (t) => { + t.plan(2) + + const users = [] + for (let i = 0; i < 100; i++) users.push({ name: 'user' + i, password: 'secret' + i }) + + const schema = { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' } + } + } + } + + // An unrelated build() opts into json-stringify for its own schema. + const optedIn = build({ type: 'array' }, { largeArrayMechanism: 'json-stringify', largeArraySize: 100 }) + t.assert.equal(optedIn(users).includes('password'), true) + + // A later build() without options must not inherit the mechanism. + const stringify = build(schema) + t.assert.equal(stringify(users).includes('password'), false) +}) + function buildTest (schema, toStringify, options) { test(`render a ${schema.title} as JSON`, (t) => { t.plan(3)