fast-json-stringify
Advanced tools
| 'use strict' | ||
| const { test } = require('node:test') | ||
| const build = require('..') | ||
| test('additionalProperties: false', (t) => { | ||
| t.plan(1) | ||
| const stringify = build({ | ||
| title: 'additionalProperties', | ||
| type: 'object', | ||
| properties: { | ||
| foo: { | ||
| type: 'string' | ||
| } | ||
| }, | ||
| additionalProperties: false | ||
| }) | ||
| const obj = { foo: 'a', bar: 'b', baz: 'c' } | ||
| t.assert.equal(stringify(obj), '{"foo":"a"}') | ||
| }) | ||
| test('additionalProperties: {}', (t) => { | ||
| t.plan(1) | ||
| const stringify = build({ | ||
| title: 'additionalProperties', | ||
| type: 'object', | ||
| properties: { | ||
| foo: { | ||
| type: 'string' | ||
| } | ||
| }, | ||
| additionalProperties: {} | ||
| }) | ||
| const obj = { foo: 'a', bar: 'b', baz: 'c' } | ||
| t.assert.equal(stringify(obj), '{"foo":"a","bar":"b","baz":"c"}') | ||
| }) | ||
| test('additionalProperties: {type: string}', (t) => { | ||
| t.plan(1) | ||
| const stringify = build({ | ||
| title: 'additionalProperties', | ||
| type: 'object', | ||
| properties: { | ||
| foo: { | ||
| type: 'string' | ||
| } | ||
| }, | ||
| additionalProperties: { | ||
| type: 'string' | ||
| } | ||
| }) | ||
| const obj = { foo: 'a', bar: 'b', baz: 'c' } | ||
| t.assert.equal(stringify(obj), '{"foo":"a","bar":"b","baz":"c"}') | ||
| }) |
@@ -17,2 +17,7 @@ name: CI | ||
| # This allows a subsequently queued workflow run to interrupt previous runs | ||
| concurrency: | ||
| group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" | ||
| cancel-in-progress: true | ||
| permissions: | ||
@@ -19,0 +24,0 @@ contents: read |
| 'use strict' | ||
| const benchmark = require('benchmark') | ||
| const suite = new benchmark.Suite() | ||
| const { Bench } = require('tinybench') | ||
| const suite = new Bench({ | ||
| name: 'Library Comparison Benchmarks', | ||
| setup: (_task, mode) => { | ||
| // Run the garbage collector before warmup at each cycle | ||
| if (mode === 'warmup' && typeof globalThis.gc === 'function') { | ||
| globalThis.gc() | ||
| } | ||
| } | ||
| }) | ||
@@ -303,8 +311,14 @@ const STR_LEN = 1e4 | ||
| suite.on('cycle', cycle) | ||
| suite.run().then(() => { | ||
| for (const task of suite.tasks) { | ||
| const hz = task.result.hz // ops/sec | ||
| const rme = task.result.rme // relative margin of error (%) | ||
| const samples = task.result.samples.length | ||
| suite.run() | ||
| const formattedHz = hz.toLocaleString('en-US', { maximumFractionDigits: 0 }) | ||
| const formattedRme = rme.toFixed(2) | ||
| function cycle (e) { | ||
| console.log(e.target.toString()) | ||
| } | ||
| const output = `${task.name} x ${formattedHz} ops/sec ±${formattedRme}% (${samples} runs sampled)` | ||
| console.log(output) | ||
| } | ||
| }).catch(err => console.error(`Error: ${err.message}`)) |
@@ -5,6 +5,13 @@ 'use strict' | ||
| const Benchmark = require('benchmark') | ||
| Benchmark.options.minSamples = 100 | ||
| const { Bench } = require('tinybench') | ||
| const suite = Benchmark.Suite() | ||
| const bench = new Bench({ | ||
| name: benchmark.name, | ||
| setup: (_task, mode) => { | ||
| // Run the garbage collector before warmup at each cycle | ||
| if (mode === 'warmup' && typeof globalThis.gc === 'function') { | ||
| globalThis.gc() | ||
| } | ||
| } | ||
| }) | ||
@@ -14,10 +21,15 @@ const FJS = require('..') | ||
| suite | ||
| .add(benchmark.name, () => { | ||
| stringify(benchmark.input) | ||
| }) | ||
| .on('cycle', (event) => { | ||
| parentPort.postMessage(String(event.target)) | ||
| }) | ||
| .on('complete', () => {}) | ||
| .run() | ||
| bench.add(benchmark.name, () => { | ||
| stringify(benchmark.input) | ||
| }).run().then(() => { | ||
| const task = bench.tasks[0] | ||
| const hz = task.result.hz // ops/sec | ||
| const rme = task.result.rme // relative margin of error (%) | ||
| const samples = task.result.samples.length | ||
| const formattedHz = hz.toLocaleString('en-US', { maximumFractionDigits: 0 }) | ||
| const formattedRme = rme.toFixed(2) | ||
| const output = `${task.name} x ${formattedHz} ops/sec ±${formattedRme}% (${samples} runs sampled)` | ||
| parentPort.postMessage(output) | ||
| }).catch(err => parentPort.postMessage(`Error: ${err.message}`)) |
+330
-132
@@ -33,3 +33,3 @@ 'use strict' | ||
| const validRoundingMethods = [ | ||
| const validRoundingMethods = new Set([ | ||
| 'floor', | ||
@@ -39,8 +39,8 @@ 'ceil', | ||
| 'trunc' | ||
| ] | ||
| ]) | ||
| const validLargeArrayMechanisms = [ | ||
| const validLargeArrayMechanisms = new Set([ | ||
| 'default', | ||
| 'json-stringify' | ||
| ] | ||
| ]) | ||
@@ -99,2 +99,10 @@ let schemaIdCounter = 0 | ||
| function getSafeSchemaRef (context, location) { | ||
| let schemaRef = location.getSchemaRef() || '' | ||
| if (schemaRef.startsWith(context.rootSchemaId)) { | ||
| schemaRef = schemaRef.replace(context.rootSchemaId, '') || '#' | ||
| } | ||
| return schemaRef | ||
| } | ||
| function build (schema, options) { | ||
@@ -113,3 +121,7 @@ isValidSchema(schema) | ||
| validatorSchemasIds: new Set(), | ||
| mergedSchemasIds: new Map() | ||
| mergedSchemasIds: new Map(), | ||
| recursiveSchemas: new Set(), | ||
| recursivePaths: new Set(), | ||
| buildingSet: new Set(), | ||
| uid: 0 | ||
| } | ||
@@ -134,3 +146,3 @@ | ||
| if (options.rounding) { | ||
| if (!validRoundingMethods.includes(options.rounding)) { | ||
| if (!validRoundingMethods.has(options.rounding)) { | ||
| throw new Error(`Unsupported integer rounding method ${options.rounding}`) | ||
@@ -141,3 +153,3 @@ } | ||
| if (options.largeArrayMechanism) { | ||
| if (validLargeArrayMechanisms.includes(options.largeArrayMechanism)) { | ||
| if (validLargeArrayMechanisms.has(options.largeArrayMechanism)) { | ||
| largeArrayMechanism = options.largeArrayMechanism | ||
@@ -150,7 +162,10 @@ } else { | ||
| if (options.largeArraySize) { | ||
| if (typeof options.largeArraySize === 'string' && Number.isFinite(Number.parseInt(options.largeArraySize, 10))) { | ||
| largeArraySize = Number.parseInt(options.largeArraySize, 10) | ||
| } else if (typeof options.largeArraySize === 'number' && Number.isInteger(options.largeArraySize)) { | ||
| const largeArraySizeType = typeof options.largeArraySize | ||
| let parsedNumber | ||
| if (largeArraySizeType === 'string' && Number.isFinite((parsedNumber = Number.parseInt(options.largeArraySize, 10)))) { | ||
| largeArraySize = parsedNumber | ||
| } else if (largeArraySizeType === 'number' && Number.isInteger(options.largeArraySize)) { | ||
| largeArraySize = options.largeArraySize | ||
| } else if (typeof options.largeArraySize === 'bigint') { | ||
| } else if (largeArraySizeType === 'bigint') { | ||
| largeArraySize = Number(options.largeArraySize) | ||
@@ -163,2 +178,3 @@ } else { | ||
| const location = new Location(schema, context.rootSchemaId) | ||
| detectRecursiveSchemas(context, location) | ||
| const code = buildValue(context, location, 'input') | ||
@@ -294,3 +310,3 @@ | ||
| function buildExtraObjectPropertiesSerializer (context, location, addComma) { | ||
| function buildExtraObjectPropertiesSerializer (context, location, addComma, objVar) { | ||
| const schema = location.schema | ||
@@ -301,3 +317,3 @@ const propertiesKeys = Object.keys(schema.properties || {}) | ||
| const propertiesKeys = ${JSON.stringify(propertiesKeys)} | ||
| for (const [key, value] of Object.entries(obj)) { | ||
| for (const [key, value] of Object.entries(${objVar})) { | ||
| if ( | ||
@@ -354,3 +370,3 @@ propertiesKeys.includes(key) || | ||
| function buildInnerObject (context, location) { | ||
| function buildInnerObject (context, location, objVar) { | ||
| const schema = location.schema | ||
@@ -369,5 +385,4 @@ | ||
| ) | ||
| const hasRequiredProperties = requiredProperties.includes(propertiesKeys[0]) | ||
| let code = 'let value\n' | ||
| let code = '' | ||
@@ -377,12 +392,15 @@ for (const key of requiredProperties) { | ||
| const sanitizedKey = JSON.stringify(key) | ||
| code += `if (obj[${sanitizedKey}] === undefined) throw new Error('${sanitizedKey.replace(/'/g, '\\\'')} is required!')\n` | ||
| code += `if (${objVar}[${sanitizedKey}] === undefined) throw new Error('${sanitizedKey.replace(/'/g, '\\\'')} is required!')\n` | ||
| } | ||
| } | ||
| code += 'let json = JSON_STR_BEGIN_OBJECT\n' | ||
| code += 'json += JSON_STR_BEGIN_OBJECT\n' | ||
| const localUid = context.uid++ | ||
| let addComma = '' | ||
| if (!hasRequiredProperties) { | ||
| code += 'let addComma = false\n' | ||
| addComma = '!addComma && (addComma = true) || (json += JSON_STR_COMMA)' | ||
| const needsRuntimeComma = propertiesKeys.length > 1 || schema.patternProperties || (schema.additionalProperties !== undefined && schema.additionalProperties !== false) | ||
| if (needsRuntimeComma) { | ||
| code += `let addComma_${localUid} = false\n` | ||
| addComma = `!addComma_${localUid} && (addComma_${localUid} = true) || (json += JSON_STR_COMMA)` | ||
| } | ||
@@ -397,2 +415,3 @@ | ||
| const sanitizedKey = JSON.stringify(key) | ||
| const value = `value_${key.replace(/[^a-zA-Z0-9]/g, '_')}_${context.uid++}` | ||
| const defaultValue = propertyLocation.schema.default | ||
@@ -402,7 +421,7 @@ const isRequired = requiredProperties.includes(key) | ||
| code += ` | ||
| value = obj[${sanitizedKey}] | ||
| if (value !== undefined) { | ||
| const ${value} = ${objVar}[${sanitizedKey}] | ||
| if (${value} !== undefined) { | ||
| ${addComma} | ||
| json += ${JSON.stringify(sanitizedKey + ':')} | ||
| ${buildValue(context, propertyLocation, 'value')} | ||
| ${buildValue(context, propertyLocation, `${value}`)} | ||
| }` | ||
@@ -424,14 +443,10 @@ | ||
| } | ||
| if (hasRequiredProperties) { | ||
| addComma = 'json += \',\'' | ||
| } | ||
| } | ||
| if (schema.patternProperties || schema.additionalProperties) { | ||
| code += buildExtraObjectPropertiesSerializer(context, location, addComma) | ||
| code += buildExtraObjectPropertiesSerializer(context, location, addComma, objVar) | ||
| } | ||
| code += ` | ||
| return json + JSON_STR_END_OBJECT | ||
| json += JSON_STR_END_OBJECT | ||
| ` | ||
@@ -442,3 +457,3 @@ return code | ||
| function mergeLocations (context, mergedSchemaId, mergedLocations) { | ||
| for (let i = 0; i < mergedLocations.length; i++) { | ||
| for (let i = 0, mergedLocationsLength = mergedLocations.length; i < mergedLocationsLength; i++) { | ||
| const location = mergedLocations[i] | ||
@@ -505,36 +520,53 @@ const schema = location.schema | ||
| function buildObject (context, location) { | ||
| function buildObject (context, location, input) { | ||
| const schema = location.schema | ||
| if (context.functionsNamesBySchema.has(schema)) { | ||
| return context.functionsNamesBySchema.get(schema) | ||
| const funcName = context.functionsNamesBySchema.get(schema) | ||
| return `json += ${funcName}(${input})` | ||
| } | ||
| const functionName = generateFuncName(context) | ||
| context.functionsNamesBySchema.set(schema, functionName) | ||
| const nullable = schema.nullable === true | ||
| let schemaRef = location.getSchemaRef() | ||
| if (schemaRef.startsWith(context.rootSchemaId)) { | ||
| schemaRef = schemaRef.replace(context.rootSchemaId, '') | ||
| } | ||
| const schemaId = location.schemaId || '' | ||
| const jsonPointer = location.jsonPointer || '' | ||
| const fullPath = `${schemaId}#${jsonPointer}` | ||
| let functionCode = ` | ||
| ` | ||
| if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema)) { | ||
| const functionName = generateFuncName(context) | ||
| context.functionsNamesBySchema.set(schema, functionName) | ||
| const nullable = schema.nullable === true | ||
| functionCode += ` | ||
| // ${schemaRef} | ||
| function ${functionName} (input) { | ||
| const obj = ${toJSON('input')} | ||
| ${!nullable ? 'if (obj === null) return JSON_STR_EMPTY_OBJECT' : ''} | ||
| const schemaRef = getSafeSchemaRef(context, location) | ||
| ${buildInnerObject(context, location)} | ||
| const functionCode = ` | ||
| // ${schemaRef} | ||
| function ${functionName} (input) { | ||
| const obj = ${toJSON('input')} | ||
| if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_OBJECT'} | ||
| let json = '' | ||
| ${buildInnerObject(context, location, 'obj')} | ||
| return json | ||
| } | ||
| ` | ||
| context.functions.push(functionCode) | ||
| return `json += ${functionName}(${input})` | ||
| } | ||
| context.buildingSet.add(schema) | ||
| const objVar = `obj_${context.uid++}` | ||
| const code = ` | ||
| const ${objVar} = ${toJSON(input)} | ||
| if (${objVar} === null) { | ||
| json += ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_OBJECT'} | ||
| } else { | ||
| ${buildInnerObject(context, location, objVar)} | ||
| } | ||
| ` | ||
| context.functions.push(functionCode) | ||
| return functionName | ||
| context.buildingSet.delete(schema) | ||
| return code | ||
| } | ||
| function buildArray (context, location) { | ||
| function buildArray (context, location, input) { | ||
| const schema = location.schema | ||
@@ -552,21 +584,26 @@ | ||
| if (context.functionsNamesBySchema.has(schema)) { | ||
| return context.functionsNamesBySchema.get(schema) | ||
| const funcName = context.functionsNamesBySchema.get(schema) | ||
| return `json += ${funcName}(${input})` | ||
| } | ||
| const functionName = generateFuncName(context) | ||
| context.functionsNamesBySchema.set(schema, functionName) | ||
| const nullable = schema.nullable === true | ||
| let schemaRef = location.getSchemaRef() | ||
| if (schemaRef.startsWith(context.rootSchemaId)) { | ||
| schemaRef = schemaRef.replace(context.rootSchemaId, '') | ||
| } | ||
| const schemaId = location.schemaId || '' | ||
| const jsonPointer = location.jsonPointer || '' | ||
| const fullPath = `${schemaId}#${jsonPointer}` | ||
| let functionCode = ` | ||
| if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema)) { | ||
| const functionName = generateFuncName(context) | ||
| context.functionsNamesBySchema.set(schema, functionName) | ||
| const schemaRef = getSafeSchemaRef(context, location) | ||
| let functionCode = ` | ||
| function ${functionName} (obj) { | ||
| // ${schemaRef} | ||
| let json = '' | ||
| ` | ||
| const nullable = schema.nullable === true | ||
| functionCode += ` | ||
| ${!nullable ? 'if (obj === null) return JSON_STR_EMPTY_ARRAY' : ''} | ||
| functionCode += ` | ||
| if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'} | ||
| if (!Array.isArray(obj)) { | ||
@@ -578,4 +615,4 @@ throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`) | ||
| if (!schema.additionalItems && Array.isArray(itemsSchema)) { | ||
| functionCode += ` | ||
| if (!schema.additionalItems && Array.isArray(itemsSchema)) { | ||
| functionCode += ` | ||
| if (arrayLength > ${itemsSchema.length}) { | ||
@@ -585,26 +622,103 @@ throw new Error(\`Item at ${itemsSchema.length} does not match schema definition.\`) | ||
| ` | ||
| } | ||
| if (largeArrayMechanism === 'json-stringify') { | ||
| functionCode += `if (arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n` | ||
| } | ||
| functionCode += ` | ||
| json += JSON_STR_BEGIN_ARRAY | ||
| ` | ||
| if (Array.isArray(itemsSchema)) { | ||
| for (let i = 0, itemsSchemaLength = itemsSchema.length; i < itemsSchemaLength; i++) { | ||
| const item = itemsSchema[i] | ||
| const value = `value_${i}` | ||
| functionCode += `const ${value} = obj[${i}]` | ||
| const tmpRes = buildValue(context, itemsLocation.getPropertyLocation(i), value) | ||
| functionCode += ` | ||
| if (${i} < arrayLength) { | ||
| if (${buildArrayTypeCondition(item.type, value)}) { | ||
| if (${i}) { | ||
| json += JSON_STR_COMMA | ||
| } | ||
| ${tmpRes} | ||
| } else { | ||
| throw new Error(\`Item at ${i} does not match schema definition.\`) | ||
| } | ||
| } | ||
| ` | ||
| } | ||
| if (schema.additionalItems) { | ||
| functionCode += ` | ||
| for (let i = ${itemsSchema.length}; i < arrayLength; i++) { | ||
| if (i) { | ||
| json += JSON_STR_COMMA | ||
| } | ||
| json += JSON.stringify(obj[i]) | ||
| }` | ||
| } | ||
| } else { | ||
| const code = buildValue(context, itemsLocation, 'value') | ||
| functionCode += ` | ||
| for (let i = 0; i < arrayLength; i++) { | ||
| if (i) { | ||
| json += JSON_STR_COMMA | ||
| } | ||
| const value = obj[i] | ||
| ${code} | ||
| }` | ||
| } | ||
| functionCode += ` | ||
| return json + JSON_STR_END_ARRAY | ||
| }` | ||
| context.functions.push(functionCode) | ||
| return `json += ${functionName}(${input})` | ||
| } | ||
| context.buildingSet.add(schema) | ||
| const safeSchemaRef = getSafeSchemaRef(context, location) | ||
| const objVar = `obj_${context.uid++}` | ||
| let inlinedCode = ` | ||
| const ${objVar} = ${input} | ||
| if (${objVar} === null) { | ||
| json += ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'} | ||
| } else if (!Array.isArray(${objVar})) { | ||
| throw new TypeError(\`The value of '${safeSchemaRef}' does not match schema definition.\`) | ||
| } else { | ||
| const arrayLength_${objVar} = ${objVar}.length | ||
| ` | ||
| if (!schema.additionalItems && Array.isArray(itemsSchema)) { | ||
| inlinedCode += ` | ||
| if (arrayLength_${objVar} > ${itemsSchema.length}) { | ||
| throw new Error(\`Item at ${itemsSchema.length} does not match schema definition.\`) | ||
| } | ||
| ` | ||
| } | ||
| if (largeArrayMechanism === 'json-stringify') { | ||
| functionCode += `if (arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n` | ||
| inlinedCode += `if (arrayLength_${objVar} >= ${largeArraySize}) json += JSON.stringify(${objVar})\n else {` | ||
| } | ||
| functionCode += ` | ||
| const arrayEnd = arrayLength - 1 | ||
| let value | ||
| let json = '' | ||
| inlinedCode += ` | ||
| json += JSON_STR_BEGIN_ARRAY | ||
| ` | ||
| if (Array.isArray(itemsSchema)) { | ||
| for (let i = 0; i < itemsSchema.length; i++) { | ||
| const localUid = context.uid++ | ||
| inlinedCode += `let addComma_${localUid} = false\n` | ||
| for (let i = 0, itemsSchemaLength = itemsSchema.length; i < itemsSchemaLength; i++) { | ||
| const item = itemsSchema[i] | ||
| functionCode += `value = obj[${i}]` | ||
| const tmpRes = buildValue(context, itemsLocation.getPropertyLocation(i), 'value') | ||
| functionCode += ` | ||
| if (${i} < arrayLength) { | ||
| if (${buildArrayTypeCondition(item.type, 'value')}) { | ||
| const value = `value_${i}_${context.uid++}` | ||
| inlinedCode += `const ${value} = ${objVar}[${i}]` | ||
| const tmpRes = buildValue(context, itemsLocation.getPropertyLocation(i), value) | ||
| inlinedCode += ` | ||
| if (${i} < arrayLength_${objVar}) { | ||
| if (${buildArrayTypeCondition(item.type, value)}) { | ||
| !addComma_${localUid} && (addComma_${localUid} = true) || (json += JSON_STR_COMMA) | ||
| ${tmpRes} | ||
| if (${i} < arrayEnd) { | ||
| json += JSON_STR_COMMA | ||
| } | ||
| } else { | ||
@@ -618,9 +732,6 @@ throw new Error(\`Item at ${i} does not match schema definition.\`) | ||
| if (schema.additionalItems) { | ||
| functionCode += ` | ||
| for (let i = ${itemsSchema.length}; i < arrayLength; i++) { | ||
| value = obj[i] | ||
| json += JSON.stringify(value) | ||
| if (i < arrayEnd) { | ||
| json += JSON_STR_COMMA | ||
| } | ||
| inlinedCode += ` | ||
| for (let i = ${itemsSchema.length}; i < arrayLength_${objVar}; i++) { | ||
| !addComma_${localUid} && (addComma_${localUid} = true) || (json += JSON_STR_COMMA) | ||
| json += JSON.stringify(${objVar}[i]) | ||
| }` | ||
@@ -630,18 +741,23 @@ } | ||
| const code = buildValue(context, itemsLocation, 'value') | ||
| functionCode += ` | ||
| for (let i = 0; i < arrayLength; i++) { | ||
| value = obj[i] | ||
| ${code} | ||
| if (i < arrayEnd) { | ||
| inlinedCode += ` | ||
| for (let i = 0; i < arrayLength_${objVar}; i++) { | ||
| if (i) { | ||
| json += JSON_STR_COMMA | ||
| } | ||
| const value = ${objVar}[i] | ||
| ${code} | ||
| }` | ||
| } | ||
| functionCode += ` | ||
| return JSON_STR_BEGIN_ARRAY + json + JSON_STR_END_ARRAY | ||
| }` | ||
| inlinedCode += ` | ||
| json += JSON_STR_END_ARRAY | ||
| ` | ||
| context.functions.push(functionCode) | ||
| return functionName | ||
| if (largeArrayMechanism === 'json-stringify') { | ||
| inlinedCode += '}' | ||
| } | ||
| inlinedCode += '}' | ||
| context.buildingSet.delete(schema) | ||
| return inlinedCode | ||
| } | ||
@@ -653,29 +769,29 @@ | ||
| case 'null': | ||
| condition = 'value === null' | ||
| condition = `${accessor} === null` | ||
| break | ||
| case 'string': | ||
| condition = `typeof value === 'string' || | ||
| value === null || | ||
| value instanceof Date || | ||
| value instanceof RegExp || | ||
| condition = `typeof ${accessor} === 'string' || | ||
| ${accessor} === null || | ||
| ${accessor} instanceof Date || | ||
| ${accessor} instanceof RegExp || | ||
| ( | ||
| typeof value === "object" && | ||
| typeof value.toString === "function" && | ||
| value.toString !== Object.prototype.toString | ||
| typeof ${accessor} === "object" && | ||
| typeof ${accessor}.toString === "function" && | ||
| ${accessor}.toString !== Object.prototype.toString | ||
| )` | ||
| break | ||
| case 'integer': | ||
| condition = 'Number.isInteger(value)' | ||
| condition = `Number.isInteger(${accessor})` | ||
| break | ||
| case 'number': | ||
| condition = 'Number.isFinite(value)' | ||
| condition = `Number.isFinite(${accessor})` | ||
| break | ||
| case 'boolean': | ||
| condition = 'typeof value === \'boolean\'' | ||
| condition = `typeof ${accessor} === 'boolean'` | ||
| break | ||
| case 'object': | ||
| condition = 'value && typeof value === \'object\' && value.constructor === Object' | ||
| condition = `${accessor} && typeof ${accessor} === 'object' && ${accessor}.constructor === Object` | ||
| break | ||
| case 'array': | ||
| condition = 'Array.isArray(value)' | ||
| condition = `Array.isArray(${accessor})` | ||
| break | ||
@@ -711,4 +827,5 @@ default: | ||
| code += ` | ||
| ${statement} (${input} === null) | ||
| ${statement} (${input} === null) { | ||
| ${nestedResult} | ||
| } | ||
| ` | ||
@@ -728,4 +845,5 @@ break | ||
| ) | ||
| ) | ||
| ) { | ||
| ${nestedResult} | ||
| } | ||
| ` | ||
@@ -736,4 +854,5 @@ break | ||
| code += ` | ||
| ${statement}(Array.isArray(${input})) | ||
| ${statement}(Array.isArray(${input})) { | ||
| ${nestedResult} | ||
| } | ||
| ` | ||
@@ -744,4 +863,5 @@ break | ||
| code += ` | ||
| ${statement}(Number.isInteger(${input}) || ${input} === null) | ||
| ${statement}(Number.isInteger(${input}) || ${input} === null) { | ||
| ${nestedResult} | ||
| } | ||
| ` | ||
@@ -752,4 +872,5 @@ break | ||
| code += ` | ||
| ${statement}(typeof ${input} === "${type}" || ${input} === null) | ||
| ${statement}(typeof ${input} === "${type}" || ${input} === null) { | ||
| ${nestedResult} | ||
| } | ||
| ` | ||
@@ -760,8 +881,4 @@ break | ||
| }) | ||
| let schemaRef = location.getSchemaRef() | ||
| if (schemaRef.startsWith(context.rootSchemaId)) { | ||
| schemaRef = schemaRef.replace(context.rootSchemaId, '') | ||
| } | ||
| code += ` | ||
| else throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`) | ||
| else throw new TypeError(\`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.\`) | ||
| ` | ||
@@ -812,8 +929,6 @@ | ||
| case 'object': { | ||
| const funcName = buildObject(context, location) | ||
| return `json += ${funcName}(${input})` | ||
| return buildObject(context, location, input) | ||
| } | ||
| case 'array': { | ||
| const funcName = buildArray(context, location) | ||
| return `json += ${funcName}(${input})` | ||
| return buildArray(context, location, input) | ||
| } | ||
@@ -827,2 +942,88 @@ case undefined: | ||
| function detectRecursiveSchemas (context, location) { | ||
| const pathStack = new Set() | ||
| function traverse (location) { | ||
| const schema = location.schema | ||
| if (typeof schema !== 'object' || schema === null) return | ||
| const schemaId = location.schemaId || '' | ||
| const jsonPointer = location.jsonPointer || '' | ||
| const fullPath = `${schemaId}#${jsonPointer}` | ||
| if (pathStack.has(fullPath)) { | ||
| // Mark all nodes in the current path that are part of the cycle | ||
| let inCycle = false | ||
| for (const p of pathStack) { | ||
| if (p === fullPath) inCycle = true | ||
| if (inCycle) context.recursivePaths.add(p) | ||
| } | ||
| context.recursivePaths.add(fullPath) | ||
| return | ||
| } | ||
| pathStack.add(fullPath) | ||
| if (schema.$ref) { | ||
| try { | ||
| const res = resolveRef(context, location) | ||
| traverse(res) | ||
| } catch (err) { | ||
| // Validation will handle missing refs later | ||
| } | ||
| } | ||
| if (schema.properties) { | ||
| const propertiesLocation = location.getPropertyLocation('properties') | ||
| for (const key in schema.properties) { | ||
| traverse(propertiesLocation.getPropertyLocation(key)) | ||
| } | ||
| } | ||
| if (schema.additionalProperties && typeof schema.additionalProperties === 'object') { | ||
| traverse(location.getPropertyLocation('additionalProperties')) | ||
| } | ||
| if (schema.patternProperties) { | ||
| const patternPropertiesLocation = location.getPropertyLocation('patternProperties') | ||
| for (const key in schema.patternProperties) { | ||
| traverse(patternPropertiesLocation.getPropertyLocation(key)) | ||
| } | ||
| } | ||
| if (schema.items) { | ||
| const itemsLocation = location.getPropertyLocation('items') | ||
| if (Array.isArray(schema.items)) { | ||
| for (let i = 0; i < schema.items.length; i++) { | ||
| traverse(itemsLocation.getPropertyLocation(i)) | ||
| } | ||
| } else { | ||
| traverse(itemsLocation) | ||
| } | ||
| } | ||
| if (schema.additionalItems && typeof schema.additionalItems === 'object') { | ||
| traverse(location.getPropertyLocation('additionalItems')) | ||
| } | ||
| if (schema.oneOf) { | ||
| const oneOfLocation = location.getPropertyLocation('oneOf') | ||
| for (let i = 0; i < schema.oneOf.length; i++) { | ||
| traverse(oneOfLocation.getPropertyLocation(i)) | ||
| } | ||
| } | ||
| if (schema.anyOf) { | ||
| const anyOfLocation = location.getPropertyLocation('anyOf') | ||
| for (let i = 0; i < schema.anyOf.length; i++) { | ||
| traverse(anyOfLocation.getPropertyLocation(i)) | ||
| } | ||
| } | ||
| if (schema.allOf) { | ||
| const allOfLocation = location.getPropertyLocation('allOf') | ||
| for (let i = 0; i < schema.allOf.length; i++) { | ||
| traverse(allOfLocation.getPropertyLocation(i)) | ||
| } | ||
| } | ||
| if (schema.then) traverse(location.getPropertyLocation('then')) | ||
| if (schema.else) traverse(location.getPropertyLocation('else')) | ||
| pathStack.delete(fullPath) | ||
| } | ||
| traverse(location) | ||
| } | ||
| function buildConstSerializer (location, input) { | ||
@@ -877,3 +1078,3 @@ const schema = location.schema | ||
| const allOfsLocation = location.getPropertyLocation('allOf') | ||
| for (let i = 0; i < allOf.length; i++) { | ||
| for (let i = 0, allOfLength = allOf.length; i < allOfLength; i++) { | ||
| locations.push(allOfsLocation.getPropertyLocation(i)) | ||
@@ -903,3 +1104,3 @@ } | ||
| for (let index = 0; index < oneOfs.length; index++) { | ||
| for (let index = 0, oneOfsLength = oneOfs.length; index < oneOfsLength; index++) { | ||
| const optionLocation = oneOfsLocation.getPropertyLocation(index) | ||
@@ -924,15 +1125,12 @@ const optionSchema = optionLocation.schema | ||
| const schemaRef = optionLocation.getSchemaRef() | ||
| code += ` | ||
| ${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input})) | ||
| ${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input})) { | ||
| ${nestedResult} | ||
| } | ||
| ` | ||
| } | ||
| let schemaRef = location.getSchemaRef() | ||
| if (schemaRef.startsWith(context.rootSchemaId)) { | ||
| schemaRef = schemaRef.replace(context.rootSchemaId, '') | ||
| } | ||
| code += ` | ||
| else throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`) | ||
| else throw new TypeError(\`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.\`) | ||
| ` | ||
@@ -939,0 +1137,0 @@ |
+6
-4
@@ -24,4 +24,4 @@ 'use strict' | ||
| errors: false, | ||
| validate: (_type, date) => { | ||
| return date instanceof Date | ||
| validate: (_type, data) => { | ||
| return data && typeof data.toJSON === 'function' | ||
| } | ||
@@ -55,4 +55,6 @@ }) | ||
| // Ajv does not support js date format. In order to properly validate objects containing a date, | ||
| // it needs to replace all occurrences of the string date format with a custom keyword fjs_type. | ||
| // Ajv does not natively support JavaScript objects like Date or other types | ||
| // that rely on a custom .toJSON() representation. To properly validate schemas | ||
| // that may contain such objects (e.g. Date, ObjectId, etc.), we replace all | ||
| // occurrences of the string type with a custom keyword fjs_type | ||
| // (see https://github.com/fastify/fast-json-stringify/pull/441) | ||
@@ -59,0 +61,0 @@ convertSchemaToAjvFormat (schema) { |
+4
-4
| { | ||
| "name": "fast-json-stringify", | ||
| "version": "6.1.1", | ||
| "version": "6.2.0", | ||
| "description": "Stringify your JSON at max speed", | ||
@@ -9,6 +9,6 @@ "main": "index.js", | ||
| "scripts": { | ||
| "bench": "node ./benchmark/bench.js", | ||
| "bench": "node --expose-gc ./benchmark/bench.js", | ||
| "bench:cmp": "node ./benchmark/bench-cmp-branch.js", | ||
| "bench:cmp:ci": "node ./benchmark/bench-cmp-branch.js --ci", | ||
| "benchmark": "node ./benchmark/bench-cmp-lib.js", | ||
| "benchmark": "node --expose-gc ./benchmark/bench-cmp-lib.js", | ||
| "lint": "eslint", | ||
@@ -67,3 +67,2 @@ "lint:fix": "eslint --fix", | ||
| "@sinclair/typebox": "^0.34.3", | ||
| "benchmark": "^2.1.4", | ||
| "c8": "^10.1.2", | ||
@@ -78,2 +77,3 @@ "cli-select": "^1.1.2", | ||
| "simple-git": "^3.23.0", | ||
| "tinybench": "^5.0.1", | ||
| "tsd": "^0.32.0", | ||
@@ -80,0 +80,0 @@ "webpack": "^5.90.3" |
+0
-1
@@ -10,3 +10,2 @@ # fast-json-stringify | ||
| Its performance advantage shrinks as your payload grows. | ||
| It pairs well with [__flatstr__](https://www.npmjs.com/package/flatstr), which triggers a V8 optimization that improves performance when eventually converting the string to a `Buffer`. | ||
@@ -13,0 +12,0 @@ |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
389619
2.51%85
1.19%14251
1.8%744
-0.13%23
15%