fast-json-stringify
Advanced tools
| name: Lock Threads | ||
| on: | ||
| schedule: | ||
| - cron: '0 0 1 * *' | ||
| workflow_dispatch: | ||
| concurrency: | ||
| group: lock | ||
| permissions: | ||
| contents: read | ||
| jobs: | ||
| lock-threads: | ||
| permissions: | ||
| issues: write | ||
| pull-requests: write | ||
| uses: fastify/workflows/.github/workflows/lock-threads.yml@v6 |
| 'use strict' | ||
| const { test } = require('node:test') | ||
| const build = require('..') | ||
| test('external $ref schema should be reused, not inlined at every reference', (t) => { | ||
| t.plan(3) | ||
| const contactSchema = { | ||
| $id: 'contact.json', | ||
| type: 'object', | ||
| properties: { | ||
| firstName: { type: 'string' }, | ||
| lastName: { type: 'string' }, | ||
| email: { type: 'string' } | ||
| } | ||
| } | ||
| // Schema referencing the same external $ref multiple times | ||
| const parentSchema = { | ||
| type: 'object', | ||
| properties: { | ||
| owner: { $ref: 'contact.json' }, | ||
| assignee: { $ref: 'contact.json' }, | ||
| reporter: { $ref: 'contact.json' } | ||
| } | ||
| } | ||
| const serializer = build(parentSchema, { schema: { 'contact.json': contactSchema } }) | ||
| const code = serializer.toString() | ||
| // The serialized function should contain extracted anonymous functions | ||
| // rather than inlining the full contact schema at every reference. | ||
| // With the fix, `firstName` appears 0 times in the main serializer body | ||
| // (it is inside the extracted function, which is not included in .toString()) | ||
| // or at most 1 time if in a single extracted function. | ||
| const firstNameMatches = code.match(/firstName/g) | ||
| t.assert.ok(firstNameMatches === null || firstNameMatches.length <= 1, | ||
| `firstName should appear at most once (extracted function), got ${firstNameMatches ? firstNameMatches.length : 0}`) | ||
| // Verify correct serialization | ||
| const data = { | ||
| owner: { firstName: 'John', lastName: 'Doe', email: 'john@example.com' }, | ||
| assignee: { firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' }, | ||
| reporter: { firstName: 'Bob', lastName: 'Jones', email: 'bob@example.com' } | ||
| } | ||
| const output = serializer(data) | ||
| t.assert.doesNotThrow(() => JSON.parse(output)) | ||
| t.assert.equal(output, JSON.stringify(data)) | ||
| }) | ||
| test('external $ref schema reused with anyOf wrapping', (t) => { | ||
| t.plan(2) | ||
| const contactSchema = { | ||
| $id: 'contact.json', | ||
| type: 'object', | ||
| properties: { | ||
| firstName: { type: 'string' }, | ||
| lastName: { type: 'string' }, | ||
| email: { type: 'string' } | ||
| } | ||
| } | ||
| // Common pattern: anyOf wrapping $ref for polymorphic fields (populated object OR string ID OR null) | ||
| const parentSchema = { | ||
| type: 'object', | ||
| properties: { | ||
| owner: { | ||
| anyOf: [{ type: ['string', 'null'] }, { $ref: 'contact.json' }] | ||
| }, | ||
| assignee: { | ||
| anyOf: [{ type: ['string', 'null'] }, { $ref: 'contact.json' }] | ||
| }, | ||
| reporter: { | ||
| anyOf: [{ type: ['string', 'null'] }, { $ref: 'contact.json' }] | ||
| } | ||
| } | ||
| } | ||
| const serializer = build(parentSchema, { schema: { 'contact.json': contactSchema } }) | ||
| // Serialize with populated objects | ||
| const data = { | ||
| owner: { firstName: 'John', lastName: 'Doe', email: 'john@example.com' }, | ||
| assignee: { firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' }, | ||
| reporter: { firstName: 'Bob', lastName: 'Jones', email: 'bob@example.com' } | ||
| } | ||
| const output = serializer(data) | ||
| t.assert.doesNotThrow(() => JSON.parse(output)) | ||
| t.assert.equal(output, JSON.stringify(data)) | ||
| }) | ||
| test('external $ref schema reused in array items', (t) => { | ||
| t.plan(2) | ||
| const contactSchema = { | ||
| $id: 'contact.json', | ||
| type: 'object', | ||
| properties: { | ||
| firstName: { type: 'string' }, | ||
| lastName: { type: 'string' } | ||
| } | ||
| } | ||
| const parentSchema = { | ||
| type: 'object', | ||
| properties: { | ||
| contacts: { | ||
| type: 'array', | ||
| items: { $ref: 'contact.json' } | ||
| }, | ||
| primary: { $ref: 'contact.json' }, | ||
| secondary: { $ref: 'contact.json' } | ||
| } | ||
| } | ||
| const serializer = build(parentSchema, { schema: { 'contact.json': contactSchema } }) | ||
| const data = { | ||
| contacts: [ | ||
| { firstName: 'Alice', lastName: 'Wonder' }, | ||
| { firstName: 'Bob', lastName: 'Builder' } | ||
| ], | ||
| primary: { firstName: 'Charlie', lastName: 'Charm' }, | ||
| secondary: { firstName: 'Diana', lastName: 'Prince' } | ||
| } | ||
| const output = serializer(data) | ||
| t.assert.doesNotThrow(() => JSON.parse(output)) | ||
| t.assert.equal(output, JSON.stringify(data)) | ||
| }) | ||
| test('external array $ref schema should be reused, not inlined at every reference', (t) => { | ||
| t.plan(2) | ||
| const tagsSchema = { | ||
| $id: 'tags.json', | ||
| type: 'array', | ||
| items: { type: 'string' } | ||
| } | ||
| const parentSchema = { | ||
| type: 'object', | ||
| properties: { | ||
| a: { $ref: 'tags.json' }, | ||
| b: { $ref: 'tags.json' }, | ||
| c: { $ref: 'tags.json' } | ||
| } | ||
| } | ||
| const serializer = build(parentSchema, { schema: { 'tags.json': tagsSchema } }) | ||
| const data = { | ||
| a: ['x', 'y'], | ||
| b: ['z'], | ||
| c: [] | ||
| } | ||
| const output = serializer(data) | ||
| t.assert.doesNotThrow(() => JSON.parse(output)) | ||
| t.assert.equal(output, JSON.stringify(data)) | ||
| }) | ||
| test('inline anonymous schemas should still be inlined (not extracted)', (t) => { | ||
| t.plan(1) | ||
| // Inline schemas (no external $ref) should continue to work normally | ||
| const schema = { | ||
| type: 'object', | ||
| properties: { | ||
| a: { type: 'object', properties: { x: { type: 'string' } } }, | ||
| b: { type: 'object', properties: { x: { type: 'string' } } } | ||
| } | ||
| } | ||
| const serializer = build(schema) | ||
| const output = serializer({ a: { x: 'hello' }, b: { x: 'world' } }) | ||
| t.assert.equal(output, '{"a":{"x":"hello"},"b":{"x":"world"}}') | ||
| }) |
| // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Test using this disabled, see https://github.com/fastify/fast-json-stringify/pull/683 | ||
| import Ajv from 'ajv' | ||
| import build, { restore, Schema, validLargeArrayMechanisms } from '..' | ||
| import { expect } from 'tstyche' | ||
| // Number schemas | ||
| build({ | ||
| type: 'number' | ||
| })(25) | ||
| build({ | ||
| type: 'integer' | ||
| })(-5) | ||
| build({ | ||
| type: 'integer' | ||
| })(5n) | ||
| build({ | ||
| type: 'number' | ||
| }, { rounding: 'ceil' }) | ||
| build({ | ||
| type: 'number' | ||
| }, { rounding: 'floor' }) | ||
| build({ | ||
| type: 'number' | ||
| }, { rounding: 'round' }) | ||
| build({ | ||
| type: 'number' | ||
| }, { rounding: 'trunc' }) | ||
| expect(build).type.not.toBeCallableWith({ | ||
| type: 'number' | ||
| }, { rounding: 'invalid' }) | ||
| // String schema | ||
| build({ | ||
| type: 'string' | ||
| })('foobar') | ||
| // Boolean schema | ||
| build({ | ||
| type: 'boolean' | ||
| })(true) | ||
| // Null schema | ||
| build({ | ||
| type: 'null' | ||
| })(null) | ||
| // Array schemas | ||
| build({ | ||
| type: 'array', | ||
| items: { type: 'number' } | ||
| })([25]) | ||
| build({ | ||
| type: 'array', | ||
| items: [{ type: 'string' }, { type: 'integer' }] | ||
| })(['hello', 42]) | ||
| // Object schemas | ||
| build({ | ||
| type: 'object' | ||
| })({}) | ||
| build({ | ||
| type: 'object', | ||
| properties: { | ||
| foo: { type: 'string' }, | ||
| bar: { type: 'integer' } | ||
| }, | ||
| required: ['foo'], | ||
| patternProperties: { | ||
| 'baz*': { type: 'null' } | ||
| }, | ||
| additionalProperties: { | ||
| type: 'boolean' | ||
| } | ||
| })({ foo: 'bar' }) | ||
| build({ | ||
| type: 'object', | ||
| properties: { | ||
| foo: { type: 'string' }, | ||
| bar: { type: 'integer' } | ||
| }, | ||
| required: ['foo'], | ||
| patternProperties: { | ||
| 'baz*': { type: 'null' } | ||
| }, | ||
| additionalProperties: { | ||
| type: 'boolean' | ||
| } | ||
| }, { rounding: 'floor' })({ foo: 'bar' }) | ||
| // Reference schemas | ||
| build({ | ||
| title: 'Example Schema', | ||
| definitions: { | ||
| num: { | ||
| type: 'object', | ||
| properties: { | ||
| int: { | ||
| type: 'integer' | ||
| } | ||
| } | ||
| }, | ||
| str: { | ||
| type: 'string' | ||
| }, | ||
| def: { | ||
| type: 'null' | ||
| } | ||
| }, | ||
| type: 'object', | ||
| properties: { | ||
| nickname: { | ||
| $ref: '#/definitions/str' | ||
| } | ||
| }, | ||
| patternProperties: { | ||
| num: { | ||
| $ref: '#/definitions/num' | ||
| } | ||
| }, | ||
| additionalProperties: { | ||
| $ref: '#/definitions/def' | ||
| } | ||
| })({ nickname: '', num: { int: 5 }, other: null }) | ||
| // Conditional/Combined schemas | ||
| build({ | ||
| title: 'Conditional/Combined Schema', | ||
| type: 'object', | ||
| properties: { | ||
| something: { | ||
| anyOf: [ | ||
| { type: 'string' }, | ||
| { type: 'boolean' } | ||
| ] | ||
| } | ||
| }, | ||
| if: { | ||
| properties: { | ||
| something: { type: 'string' } | ||
| } | ||
| }, | ||
| then: { | ||
| properties: { | ||
| somethingElse: { type: 'number' } | ||
| } | ||
| }, | ||
| else: { | ||
| properties: { | ||
| somethingElse: { type: 'null' } | ||
| } | ||
| } | ||
| })({ something: 'a string', somethingElse: 42 }) | ||
| // String schema with format | ||
| build({ | ||
| type: 'string', | ||
| format: 'date-time' | ||
| })(new Date()) | ||
| /* | ||
| This overload doesn't work yet - | ||
| TypeScript chooses the generic for the schema | ||
| before it chooses the overload for the options | ||
| parameter. | ||
| let str: string, ajv: Ajv | ||
| str = build({ | ||
| type: 'number' | ||
| }, { debugMode: true }).code | ||
| ajv = build({ | ||
| type: 'number' | ||
| }, { debugMode: true }).ajv | ||
| str = build({ | ||
| type: 'number' | ||
| }, { mode: 'debug' }).code | ||
| ajv = build({ | ||
| type: 'number' | ||
| }, { mode: 'debug' }).ajv | ||
| str = build({ | ||
| type: 'number' | ||
| }, { mode: 'standalone' }) | ||
| */ | ||
| const debugCompiled = build({ | ||
| title: 'default string', | ||
| type: 'object', | ||
| properties: { | ||
| firstName: { | ||
| type: 'string' | ||
| } | ||
| } | ||
| }, { mode: 'debug' }) | ||
| expect(build.restore(debugCompiled)).type.toBe(build({} as Schema)) | ||
| expect(restore(debugCompiled)).type.toBe(build({} as Schema)) | ||
| expect(build.validLargeArrayMechanisms).type.toBe<string[]>() | ||
| expect(validLargeArrayMechanisms).type.toBe<string[]>() | ||
| /** | ||
| * Schema inference | ||
| */ | ||
| // With inference | ||
| interface InferenceSchema { | ||
| id: string; | ||
| a?: number; | ||
| } | ||
| const stringify3 = build({ | ||
| type: 'object', | ||
| properties: { a: { type: 'string' } }, | ||
| }) | ||
| stringify3<InferenceSchema>({ id: '123' }) | ||
| stringify3<InferenceSchema>({ a: 123, id: '123' }) | ||
| expect(stringify3<InferenceSchema>).type.not.toBeCallableWith({ anotherOne: 'bar' }) | ||
| expect(stringify3<Schema>).type.not.toBeCallableWith({ a: 'bar' }) | ||
| // Without inference | ||
| const stringify4 = build({ | ||
| type: 'object', | ||
| properties: { a: { type: 'string' } }, | ||
| }) | ||
| stringify4({ id: '123' }) | ||
| stringify4({ a: 123, id: '123' }) | ||
| stringify4({ anotherOne: 'bar' }) | ||
| stringify4({ a: 'bar' }) | ||
| // Without inference - string type | ||
| const stringify5 = build({ | ||
| type: 'string', | ||
| }) | ||
| stringify5('foo') | ||
| expect(stringify5).type.not.toBeCallableWith({ id: '123' }) | ||
| // Without inference - null type | ||
| const stringify6 = build({ | ||
| type: 'null', | ||
| }) | ||
| stringify6(null) | ||
| expect(stringify6).type.not.toBeCallableWith('a string') | ||
| // Without inference - boolean type | ||
| const stringify7 = build({ | ||
| type: 'boolean', | ||
| }) | ||
| stringify7(true) | ||
| expect(stringify7).type.not.toBeCallableWith('a string') | ||
| // largeArrayMechanism | ||
| build({}, { largeArrayMechanism: 'json-stringify' }) | ||
| build({}, { largeArrayMechanism: 'default' }) | ||
| expect(build).type.not.toBeCallableWith({} as Schema, { largeArrayMechanism: 'invalid' }) | ||
| build({}, { largeArraySize: 2000 }) | ||
| build({}, { largeArraySize: '2e4' }) | ||
| build({}, { largeArraySize: 2n }) | ||
| expect(build).type.not.toBeCallableWith({} as Schema, { largeArraySize: ['asdf'] }) |
@@ -30,5 +30,5 @@ name: CI | ||
| pull-requests: write | ||
| uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 | ||
| uses: fastify/workflows/.github/workflows/plugins-ci.yml@v6 | ||
| with: | ||
| license-check: true | ||
| lint: true |
@@ -172,4 +172,2 @@ 'use strict' | ||
| Number(str) | ||
| for (let i = 0; i < MULTI_ARRAY_LENGTH; i++) { | ||
@@ -179,132 +177,132 @@ multiArray[i] = obj | ||
| suite.add('FJS creation', function () { | ||
| suite.add('fast-json-stringify: creation', function () { | ||
| FJS(schema) | ||
| }) | ||
| suite.add('CJS creation', function () { | ||
| suite.add('compile-json-stringify: creation', function () { | ||
| CJS(schemaCJS) | ||
| }) | ||
| suite.add('AJV Serialize creation', function () { | ||
| suite.add('AJV: creation', function () { | ||
| ajv.compileSerializer(schemaAJVJTD) | ||
| }) | ||
| suite.add('json-accelerator creation', function () { | ||
| suite.add('json-accelerator: creation', function () { | ||
| createAccelerator(schema) | ||
| }) | ||
| suite.add('JSON.stringify array', function () { | ||
| suite.add('JSON.stringify: array', function () { | ||
| JSON.stringify(multiArray) | ||
| }) | ||
| suite.add('fast-json-stringify array default', function () { | ||
| suite.add('fast-json-stringify [default]: array', function () { | ||
| stringifyArrayDefault(multiArray) | ||
| }) | ||
| suite.add('json-accelerator array', function () { | ||
| suite.add('json-accelerator: array', function () { | ||
| accelArray(multiArray) | ||
| }) | ||
| suite.add('fast-json-stringify array json-stringify', function () { | ||
| suite.add('fast-json-stringify [json-stringify]: array', function () { | ||
| stringifyArrayJSONStringify(multiArray) | ||
| }) | ||
| suite.add('compile-json-stringify array', function () { | ||
| suite.add('compile-json-stringify: array', function () { | ||
| CJSStringifyArray(multiArray) | ||
| }) | ||
| suite.add('AJV Serialize array', function () { | ||
| suite.add('AJV: array', function () { | ||
| ajvSerializeArray(multiArray) | ||
| }) | ||
| suite.add('JSON.stringify large array', function () { | ||
| suite.add('JSON.stringify: large array', function () { | ||
| JSON.stringify(largeArray) | ||
| }) | ||
| suite.add('fast-json-stringify large array default', function () { | ||
| suite.add('fast-json-stringify [default]: large array', function () { | ||
| stringifyArrayDefault(largeArray) | ||
| }) | ||
| suite.add('fast-json-stringify large array json-stringify', function () { | ||
| suite.add('fast-json-stringify [json-stringify]: large array', function () { | ||
| stringifyArrayJSONStringify(largeArray) | ||
| }) | ||
| suite.add('compile-json-stringify large array', function () { | ||
| suite.add('compile-json-stringify: large array', function () { | ||
| CJSStringifyArray(largeArray) | ||
| }) | ||
| suite.add('AJV Serialize large array', function () { | ||
| suite.add('AJV: large array', function () { | ||
| ajvSerializeArray(largeArray) | ||
| }) | ||
| suite.add('JSON.stringify long string', function () { | ||
| suite.add('JSON.stringify: long string', function () { | ||
| JSON.stringify(str) | ||
| }) | ||
| suite.add('fast-json-stringify long string', function () { | ||
| suite.add('fast-json-stringify: long string', function () { | ||
| stringifyString(str) | ||
| }) | ||
| suite.add('json-accelerator long string', function () { | ||
| suite.add('json-accelerator: long string', function () { | ||
| stringifyString(str) | ||
| }) | ||
| suite.add('compile-json-stringify long string', function () { | ||
| suite.add('compile-json-stringify: long string', function () { | ||
| CJSStringifyString(str) | ||
| }) | ||
| suite.add('AJV Serialize long string', function () { | ||
| suite.add('AJV: long string', function () { | ||
| ajvSerializeString(str) | ||
| }) | ||
| suite.add('JSON.stringify short string', function () { | ||
| suite.add('JSON.stringify: short string', function () { | ||
| JSON.stringify('hello world') | ||
| }) | ||
| suite.add('fast-json-stringify short string', function () { | ||
| suite.add('fast-json-stringify: short string', function () { | ||
| stringifyString('hello world') | ||
| }) | ||
| suite.add('json-accelerator short string', function () { | ||
| suite.add('json-accelerator: short string', function () { | ||
| accelString('hello world') | ||
| }) | ||
| suite.add('compile-json-stringify short string', function () { | ||
| suite.add('compile-json-stringify: short string', function () { | ||
| CJSStringifyString('hello world') | ||
| }) | ||
| suite.add('AJV Serialize short string', function () { | ||
| suite.add('AJV: short string', function () { | ||
| ajvSerializeString('hello world') | ||
| }) | ||
| suite.add('JSON.stringify obj', function () { | ||
| suite.add('JSON.stringify: obj', function () { | ||
| JSON.stringify(obj) | ||
| }) | ||
| suite.add('fast-json-stringify obj', function () { | ||
| suite.add('fast-json-stringify: obj', function () { | ||
| stringify(obj) | ||
| }) | ||
| suite.add('json-accelerator obj', function () { | ||
| suite.add('json-accelerator: obj', function () { | ||
| accelStringify(obj) | ||
| }) | ||
| suite.add('compile-json-stringify obj', function () { | ||
| suite.add('compile-json-stringify: obj', function () { | ||
| CJSStringify(obj) | ||
| }) | ||
| suite.add('AJV Serialize obj', function () { | ||
| suite.add('AJV: obj', function () { | ||
| ajvSerialize(obj) | ||
| }) | ||
| suite.add('JSON stringify date', function () { | ||
| suite.add('JSON.stringify: date', function () { | ||
| JSON.stringify(date) | ||
| }) | ||
| suite.add('fast-json-stringify date format', function () { | ||
| suite.add('fast-json-stringify: date', function () { | ||
| stringifyDate(date) | ||
| }) | ||
| suite.add('json-accelerate date format', function () { | ||
| suite.add('json-accelerate: date', function () { | ||
| accelDate(date) | ||
| }) | ||
| suite.add('compile-json-stringify date format', function () { | ||
| suite.add('compile-json-stringify: date', function () { | ||
| CJSStringifyDate(date) | ||
@@ -314,13 +312,31 @@ }) | ||
| suite.run().then(() => { | ||
| for (const task of suite.tasks) { | ||
| const hz = task.result.throughput.mean // ops/sec | ||
| const rme = task.result.latency.rme // relative margin of error (%) | ||
| const samples = task.result.latency.df + 1 // degrees of freedom + 1 = sample count | ||
| const results = suite.tasks.map(task => ({ | ||
| name: task.name, | ||
| hz: task.result.throughput.mean, | ||
| rme: task.result.latency.rme, | ||
| samples: task.result.latency.df + 1 | ||
| })) | ||
| const formattedHz = hz.toLocaleString('en-US', { maximumFractionDigits: 0 }) | ||
| const formattedRme = rme.toFixed(2) | ||
| const scenarios = {} | ||
| for (const result of results) { | ||
| const [library, scenario] = result.name.split(':').map(s => s.trim()) | ||
| const output = `${task.name} x ${formattedHz} ops/sec ±${formattedRme}% (${samples} runs sampled)` | ||
| console.log(output) | ||
| if (!scenarios[scenario]) scenarios[scenario] = [] | ||
| scenarios[scenario].push({ ...result, library }) | ||
| } | ||
| for (const [scenario, tasks] of Object.entries(scenarios)) { | ||
| console.log(`\n--- ${scenario} ---`) | ||
| const sorted = tasks.sort((a, b) => b.hz - a.hz) | ||
| const winner = sorted[0] | ||
| for (const task of sorted) { | ||
| const formattedHz = task.hz.toLocaleString('en-US', { maximumFractionDigits: 0 }) | ||
| const formattedRme = task.rme.toFixed(2) | ||
| const isWinner = task === winner | ||
| const prefix = isWinner ? '🏆 ' : ' ' | ||
| console.log(`${prefix}${task.library.padEnd(40)} x ${formattedHz.padStart(15)} ops/sec ±${formattedRme}% (${task.samples} runs sampled)`) | ||
| } | ||
| } | ||
| }).catch(err => console.error(`Error: ${err.message}`)) |
+2
-2
@@ -571,3 +571,3 @@ 'use strict' | ||
| if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema)) { | ||
| if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema) || schemaId !== '') { | ||
| const functionName = generateFuncName(context) | ||
@@ -631,3 +631,3 @@ context.functionsNamesBySchema.set(schema, functionName) | ||
| if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema)) { | ||
| if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema) || schemaId !== '') { | ||
| const functionName = generateFuncName(context) | ||
@@ -634,0 +634,0 @@ context.functionsNamesBySchema.set(schema, functionName) |
+1
-3
| MIT License | ||
| Copyright (c) 2016-present Matteo Collina | ||
| Copyright (c) 2016-present The Fastify team | ||
| Copyright (c) 2016-present The Fastify team <https://github.com/fastify/fastify#team> | ||
| The Fastify team members are listed at https://github.com/fastify/fastify#team. | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
@@ -9,0 +7,0 @@ of this software and associated documentation files (the "Software"), to deal |
+7
-7
| { | ||
| "name": "fast-json-stringify", | ||
| "version": "6.3.0", | ||
| "version": "6.4.0", | ||
| "description": "Stringify your JSON at max speed", | ||
@@ -15,3 +15,3 @@ "main": "index.js", | ||
| "lint:fix": "eslint --fix", | ||
| "test:typescript": "tsd", | ||
| "test:typescript": "tstyche", | ||
| "test:unit": "c8 node --test", | ||
@@ -66,4 +66,3 @@ "test": "npm run test:unit && npm run test:typescript" | ||
| "devDependencies": { | ||
| "@sinclair/typebox": "^0.34.48", | ||
| "c8": "^10.1.3", | ||
| "c8": "^11.0.0", | ||
| "cli-select": "^1.1.2", | ||
@@ -75,7 +74,8 @@ "compile-json-stringify": "^0.1.2", | ||
| "json-accelerator": "^0.1.7", | ||
| "neostandard": "^0.12.2", | ||
| "neostandard": "^0.13.0", | ||
| "simple-git": "^3.30.0", | ||
| "tinybench": "^6.0.0", | ||
| "tsd": "^0.33.0", | ||
| "typescript": "^5.9.3", | ||
| "tstyche": "^7.1.0", | ||
| "typebox": "^1.0.81", | ||
| "typescript": "~6.0.2", | ||
| "webpack": "^5.104.1" | ||
@@ -82,0 +82,0 @@ }, |
+20
-0
@@ -793,1 +793,21 @@ 'use strict' | ||
| }) | ||
| test('invalid anyOf schema', (t) => { | ||
| t.plan(1) | ||
| const schema = { | ||
| type: 'object', | ||
| properties: { | ||
| prop: { | ||
| anyOf: 'not array' // invalid, anyOf must be array | ||
| } | ||
| } | ||
| } | ||
| try { | ||
| build(schema) | ||
| t.assert.fail('Should throw') | ||
| } catch (err) { | ||
| t.assert.ok(err.message.includes('schema is invalid')) | ||
| } | ||
| }) |
+22
-0
@@ -389,2 +389,24 @@ 'use strict' | ||
| test('array items is a list of schema and additionalItems is a schema', (t) => { | ||
| t.plan(1) | ||
| const schema = { | ||
| type: 'object', | ||
| properties: { | ||
| foo: { | ||
| type: 'array', | ||
| items: [ | ||
| { type: 'string' } | ||
| ], | ||
| additionalItems: { type: 'number' } | ||
| } | ||
| } | ||
| } | ||
| const stringify = build(schema) | ||
| const result = stringify({ foo: ['foo', 42] }) | ||
| t.assert.equal(result, '{"foo":["foo",42]}') | ||
| }) | ||
| // https://github.com/fastify/fast-json-stringify/issues/279 | ||
@@ -391,0 +413,0 @@ test('object array with anyOf and symbol', (t) => { |
@@ -122,1 +122,22 @@ 'use strict' | ||
| }) | ||
| test('Serializer restoreFromState', t => { | ||
| t.plan(1) | ||
| const Serializer = require('../lib/serializer') | ||
| const serializer = new Serializer() | ||
| const state = serializer.getState() | ||
| const restored = Serializer.restoreFromState(state) | ||
| t.assert.ok(restored instanceof Serializer) | ||
| }) | ||
| test('Validator restoreFromState', t => { | ||
| t.plan(1) | ||
| const Validator = require('../lib/validator') | ||
| const validator = new Validator() | ||
| validator.addSchema({ type: 'string' }, 'test') | ||
| const state = validator.getState() | ||
| const restored = Validator.restoreFromState(state) | ||
| t.assert.ok(restored instanceof Validator) | ||
| }) |
@@ -469,1 +469,73 @@ 'use strict' | ||
| }) | ||
| test('invalid if-then-else schema', (t) => { | ||
| t.plan(1) | ||
| const schema = { | ||
| type: 'object', | ||
| if: { | ||
| type: 'object', | ||
| properties: { | ||
| kind: { type: 'string', enum: ['foobar'] } | ||
| } | ||
| }, | ||
| then: { | ||
| type: 'object', | ||
| properties: { | ||
| foo: { type: 'string' } | ||
| } | ||
| }, | ||
| else: { | ||
| type: 'object', | ||
| properties: 'invalid' // properties must be object | ||
| } | ||
| } | ||
| try { | ||
| build(schema) | ||
| t.assert.fail('Should throw') | ||
| } catch (err) { | ||
| t.assert.ok(err.message.includes('schema is invalid')) | ||
| } | ||
| }) | ||
| test('if-then-else with allOf', (t) => { | ||
| t.plan(2) | ||
| const schema = { | ||
| type: 'object', | ||
| allOf: [ | ||
| { | ||
| type: 'object', | ||
| properties: { | ||
| base: { type: 'string' } | ||
| } | ||
| } | ||
| ], | ||
| if: { | ||
| type: 'object', | ||
| properties: { | ||
| kind: { type: 'string', enum: ['foobar'] } | ||
| } | ||
| }, | ||
| then: { | ||
| type: 'object', | ||
| properties: { | ||
| foo: { type: 'string' } | ||
| } | ||
| }, | ||
| else: { | ||
| type: 'object', | ||
| properties: { | ||
| bar: { type: 'string' } | ||
| } | ||
| } | ||
| } | ||
| const stringify = build(schema) | ||
| const data = { base: 'test', kind: 'foobar', foo: 'value' } | ||
| const output = stringify(data) | ||
| t.assert.doesNotThrow(() => JSON.parse(output)) | ||
| t.assert.equal(output, '{"base":"test","foo":"value"}') | ||
| }) |
@@ -19,1 +19,21 @@ 'use strict' | ||
| }) | ||
| test('invalid not schema', (t) => { | ||
| t.plan(1) | ||
| const schema = { | ||
| type: 'object', | ||
| properties: { | ||
| prop: { | ||
| not: 'not object' // invalid, not must be object | ||
| } | ||
| } | ||
| } | ||
| try { | ||
| build(schema) | ||
| t.assert.fail('Should throw') | ||
| } catch (err) { | ||
| t.assert.ok(err.message.includes('schema is invalid')) | ||
| } | ||
| }) |
+20
-0
@@ -491,1 +491,21 @@ 'use strict' | ||
| }) | ||
| test('invalid oneOf schema', (t) => { | ||
| t.plan(1) | ||
| const schema = { | ||
| type: 'object', | ||
| properties: { | ||
| prop: { | ||
| oneOf: 'not array' // invalid, oneOf must be array | ||
| } | ||
| } | ||
| } | ||
| try { | ||
| build(schema) | ||
| t.assert.fail('Should throw') | ||
| } catch (err) { | ||
| t.assert.ok(err.message.includes('schema is invalid')) | ||
| } | ||
| }) |
+31
-0
@@ -2047,1 +2047,32 @@ 'use strict' | ||
| }) | ||
| test('ref nested', (t) => { | ||
| t.plan(2) | ||
| const schema = { | ||
| definitions: { | ||
| def1: { | ||
| $ref: '#/definitions/def2' | ||
| }, | ||
| def2: { | ||
| type: 'string' | ||
| } | ||
| }, | ||
| type: 'object', | ||
| properties: { | ||
| str: { | ||
| $ref: '#/definitions/def1' | ||
| } | ||
| } | ||
| } | ||
| const object = { | ||
| str: 'test' | ||
| } | ||
| const stringify = build(schema) | ||
| const output = stringify(object) | ||
| t.assert.doesNotThrow(() => JSON.parse(output)) | ||
| t.assert.equal(output, '{"str":"test"}') | ||
| }) |
@@ -7,3 +7,3 @@ 'use strict' | ||
| test('nested object in pattern properties for typebox', (t) => { | ||
| const { Type } = require('@sinclair/typebox') | ||
| const { Type } = require('typebox') | ||
@@ -10,0 +10,0 @@ t.plan(1) |
| # Number of days of inactivity before an issue becomes stale | ||
| daysUntilStale: 15 | ||
| # Number of days of inactivity before a stale issue is closed | ||
| daysUntilClose: 7 | ||
| # Issues with these labels will never be considered stale | ||
| exemptLabels: | ||
| - "discussion" | ||
| - "feature request" | ||
| - "bug" | ||
| - "help wanted" | ||
| - "plugin suggestion" | ||
| - "good first issue" | ||
| # Label to use when marking an issue as stale | ||
| staleLabel: stale | ||
| # Comment to post when marking an issue as stale. Set to `false` to disable | ||
| markComment: > | ||
| This issue has been automatically marked as stale because it has not had | ||
| recent activity. It will be closed if no further activity occurs. Thank you | ||
| for your contributions. | ||
| # Comment to post when closing a stale issue. Set to `false` to disable | ||
| closeComment: false |
| // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Test using this disabled, see https://github.com/fastify/fast-json-stringify/pull/683 | ||
| import Ajv from 'ajv' | ||
| import build, { restore, Schema, validLargeArrayMechanisms } from '..' | ||
| import { expectError, expectType } from 'tsd' | ||
| // Number schemas | ||
| build({ | ||
| type: 'number' | ||
| })(25) | ||
| build({ | ||
| type: 'integer' | ||
| })(-5) | ||
| build({ | ||
| type: 'integer' | ||
| })(5n) | ||
| build({ | ||
| type: 'number' | ||
| }, { rounding: 'ceil' }) | ||
| build({ | ||
| type: 'number' | ||
| }, { rounding: 'floor' }) | ||
| build({ | ||
| type: 'number' | ||
| }, { rounding: 'round' }) | ||
| build({ | ||
| type: 'number' | ||
| }, { rounding: 'trunc' }) | ||
| expectError(build({ | ||
| type: 'number' | ||
| }, { rounding: 'invalid' })) | ||
| // String schema | ||
| build({ | ||
| type: 'string' | ||
| })('foobar') | ||
| // Boolean schema | ||
| build({ | ||
| type: 'boolean' | ||
| })(true) | ||
| // Null schema | ||
| build({ | ||
| type: 'null' | ||
| })(null) | ||
| // Array schemas | ||
| build({ | ||
| type: 'array', | ||
| items: { type: 'number' } | ||
| })([25]) | ||
| build({ | ||
| type: 'array', | ||
| items: [{ type: 'string' }, { type: 'integer' }] | ||
| })(['hello', 42]) | ||
| // Object schemas | ||
| build({ | ||
| type: 'object' | ||
| })({}) | ||
| build({ | ||
| type: 'object', | ||
| properties: { | ||
| foo: { type: 'string' }, | ||
| bar: { type: 'integer' } | ||
| }, | ||
| required: ['foo'], | ||
| patternProperties: { | ||
| 'baz*': { type: 'null' } | ||
| }, | ||
| additionalProperties: { | ||
| type: 'boolean' | ||
| } | ||
| })({ foo: 'bar' }) | ||
| build({ | ||
| type: 'object', | ||
| properties: { | ||
| foo: { type: 'string' }, | ||
| bar: { type: 'integer' } | ||
| }, | ||
| required: ['foo'], | ||
| patternProperties: { | ||
| 'baz*': { type: 'null' } | ||
| }, | ||
| additionalProperties: { | ||
| type: 'boolean' | ||
| } | ||
| }, { rounding: 'floor' })({ foo: 'bar' }) | ||
| // Reference schemas | ||
| build({ | ||
| title: 'Example Schema', | ||
| definitions: { | ||
| num: { | ||
| type: 'object', | ||
| properties: { | ||
| int: { | ||
| type: 'integer' | ||
| } | ||
| } | ||
| }, | ||
| str: { | ||
| type: 'string' | ||
| }, | ||
| def: { | ||
| type: 'null' | ||
| } | ||
| }, | ||
| type: 'object', | ||
| properties: { | ||
| nickname: { | ||
| $ref: '#/definitions/str' | ||
| } | ||
| }, | ||
| patternProperties: { | ||
| num: { | ||
| $ref: '#/definitions/num' | ||
| } | ||
| }, | ||
| additionalProperties: { | ||
| $ref: '#/definitions/def' | ||
| } | ||
| })({ nickname: '', num: { int: 5 }, other: null }) | ||
| // Conditional/Combined schemas | ||
| build({ | ||
| title: 'Conditional/Combined Schema', | ||
| type: 'object', | ||
| properties: { | ||
| something: { | ||
| anyOf: [ | ||
| { type: 'string' }, | ||
| { type: 'boolean' } | ||
| ] | ||
| } | ||
| }, | ||
| if: { | ||
| properties: { | ||
| something: { type: 'string' } | ||
| } | ||
| }, | ||
| then: { | ||
| properties: { | ||
| somethingElse: { type: 'number' } | ||
| } | ||
| }, | ||
| else: { | ||
| properties: { | ||
| somethingElse: { type: 'null' } | ||
| } | ||
| } | ||
| })({ something: 'a string', somethingElse: 42 }) | ||
| // String schema with format | ||
| build({ | ||
| type: 'string', | ||
| format: 'date-time' | ||
| })(new Date()) | ||
| /* | ||
| This overload doesn't work yet - | ||
| TypeScript chooses the generic for the schema | ||
| before it chooses the overload for the options | ||
| parameter. | ||
| let str: string, ajv: Ajv | ||
| str = build({ | ||
| type: 'number' | ||
| }, { debugMode: true }).code | ||
| ajv = build({ | ||
| type: 'number' | ||
| }, { debugMode: true }).ajv | ||
| str = build({ | ||
| type: 'number' | ||
| }, { mode: 'debug' }).code | ||
| ajv = build({ | ||
| type: 'number' | ||
| }, { mode: 'debug' }).ajv | ||
| str = build({ | ||
| type: 'number' | ||
| }, { mode: 'standalone' }) | ||
| */ | ||
| const debugCompiled = build({ | ||
| title: 'default string', | ||
| type: 'object', | ||
| properties: { | ||
| firstName: { | ||
| type: 'string' | ||
| } | ||
| } | ||
| }, { mode: 'debug' }) | ||
| expectType<ReturnType<typeof build>>(build.restore(debugCompiled)) | ||
| expectType<ReturnType<typeof build>>(restore(debugCompiled)) | ||
| expectType<string[]>(build.validLargeArrayMechanisms) | ||
| expectType<string[]>(validLargeArrayMechanisms) | ||
| /** | ||
| * Schema inference | ||
| */ | ||
| // With inference | ||
| interface InferenceSchema { | ||
| id: string; | ||
| a?: number; | ||
| } | ||
| const stringify3 = build({ | ||
| type: 'object', | ||
| properties: { a: { type: 'string' } }, | ||
| }) | ||
| stringify3<InferenceSchema>({ id: '123' }) | ||
| stringify3<InferenceSchema>({ a: 123, id: '123' }) | ||
| expectError(stringify3<InferenceSchema>({ anotherOne: 'bar' })) | ||
| expectError(stringify3<Schema>({ a: 'bar' })) | ||
| // Without inference | ||
| const stringify4 = build({ | ||
| type: 'object', | ||
| properties: { a: { type: 'string' } }, | ||
| }) | ||
| stringify4({ id: '123' }) | ||
| stringify4({ a: 123, id: '123' }) | ||
| stringify4({ anotherOne: 'bar' }) | ||
| stringify4({ a: 'bar' }) | ||
| // Without inference - string type | ||
| const stringify5 = build({ | ||
| type: 'string', | ||
| }) | ||
| stringify5('foo') | ||
| expectError(stringify5({ id: '123' })) | ||
| // Without inference - null type | ||
| const stringify6 = build({ | ||
| type: 'null', | ||
| }) | ||
| stringify6(null) | ||
| expectError(stringify6('a string')) | ||
| // Without inference - boolean type | ||
| const stringify7 = build({ | ||
| type: 'boolean', | ||
| }) | ||
| stringify7(true) | ||
| expectError(stringify7('a string')) | ||
| // largeArrayMechanism | ||
| build({}, { largeArrayMechanism: 'json-stringify' }) | ||
| build({}, { largeArrayMechanism: 'default' }) | ||
| expectError(build({} as Schema, { largeArrayMechanism: 'invalid' })) | ||
| build({}, { largeArraySize: 2000 }) | ||
| build({}, { largeArraySize: '2e4' }) | ||
| build({}, { largeArraySize: 2n }) | ||
| expectError(build({} as Schema, { largeArraySize: ['asdf'] })) |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
402099
2.38%86
1.18%14681
2.4%3
-25%