├── .gitattributes ├── .github ├── dependabot.yml ├── stale.yml └── workflows │ ├── benchmark.yml │ └── ci.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── benchmark ├── bench-cmp-branch.js ├── bench-cmp-lib.js ├── bench-thread.js └── bench.js ├── build └── build-schema-validator.js ├── eslint.config.js ├── examples ├── example.js └── server.js ├── index.js ├── lib ├── location.js ├── merge-schemas.js ├── schema-validator.js ├── serializer.js ├── standalone.js └── validator.js ├── package.json ├── test ├── additionalProperties.test.js ├── allof.test.js ├── any.test.js ├── anyof.test.js ├── array.test.js ├── asNumber.test.js ├── basic.test.js ├── bigint.test.js ├── clean-cache.test.js ├── const.test.js ├── date.test.js ├── debug-mode.test.js ├── defaults.test.js ├── enum.test.js ├── fix-604.test.js ├── fixtures │ └── .keep ├── if-then-else.test.js ├── inferType.test.js ├── infinity.test.js ├── integer.test.js ├── invalidSchema.test.js ├── issue-479.test.js ├── json-schema-test-suite │ ├── README.md │ ├── draft4.test.js │ ├── draft4 │ │ └── required.json │ ├── draft6.test.js │ ├── draft6 │ │ └── required.json │ ├── draft7.test.js │ ├── draft7 │ │ └── required.json │ └── util.js ├── missing-values.test.js ├── multi-type-serializer.test.js ├── nestedObjects.test.js ├── nullable.test.js ├── oneof.test.js ├── patternProperties.test.js ├── recursion.test.js ├── ref.json ├── ref.test.js ├── regex.test.js ├── required.test.js ├── requiresAjv.test.js ├── sanitize.test.js ├── sanitize2.test.js ├── sanitize3.test.js ├── sanitize4.test.js ├── sanitize5.test.js ├── sanitize6.test.js ├── sanitize7.test.js ├── side-effect.test.js ├── standalone-mode.test.js ├── string.test.js ├── surrogate.test.js ├── toJSON.test.js ├── typebox.test.js ├── typesArray.test.js ├── unknownFormats.test.js └── webpack.test.js └── types ├── index.d.ts └── index.test-d.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically convert line endings 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - "discussion" 8 | - "feature request" 9 | - "bug" 10 | - "help wanted" 11 | - "plugin suggestion" 12 | - "good first issue" 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark PR 2 | 3 | on: 4 | pull_request_target: 5 | types: [labeled] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | benchmark: 12 | if: ${{ github.event.label.name == 'benchmark' }} 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | outputs: 17 | PR-BENCH: ${{ steps.benchmark-pr.outputs.BENCH_RESULT }} 18 | MAIN-BENCH: ${{ steps.benchmark-main.outputs.BENCH_RESULT }} 19 | steps: 20 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | with: 22 | persist-credentials: false 23 | ref: ${{github.event.pull_request.head.sha}} 24 | repository: ${{github.event.pull_request.head.repo.full_name}} 25 | 26 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 27 | with: 28 | check-latest: true 29 | node-version: 20 30 | 31 | - name: Install 32 | run: | 33 | npm install --ignore-scripts 34 | 35 | - name: Run benchmark 36 | id: benchmark-pr 37 | run: | 38 | npm run --silent bench > ./bench-result 39 | content=$(cat ./bench-result) 40 | content="${content//'%'/'%25'}" 41 | content="${content//$'\n'/'%0A'}" 42 | content="${content//$'\r'/'%0D'}" 43 | echo "::set-output name=BENCH_RESULT::$content" 44 | 45 | # main benchmark 46 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 47 | with: 48 | ref: 'main' 49 | persist-credentials: false 50 | 51 | - name: Install 52 | run: | 53 | npm install --ignore-scripts 54 | 55 | - name: Run benchmark 56 | id: benchmark-main 57 | run: | 58 | npm run --silent bench > ./bench-result 59 | content=$(cat ./bench-result) 60 | content="${content//'%'/'%25'}" 61 | content="${content//$'\n'/'%0A'}" 62 | content="${content//$'\r'/'%0D'}" 63 | echo "::set-output name=BENCH_RESULT::$content" 64 | 65 | output-benchmark: 66 | if: "always()" 67 | needs: [benchmark] 68 | runs-on: ubuntu-latest 69 | permissions: 70 | pull-requests: write 71 | steps: 72 | - name: Comment PR 73 | uses: thollander/actions-comment-pull-request@65f9e5c9a1f2cd378bd74b2e057c9736982a8e74 # v3.0.1 74 | with: 75 | github-token: ${{ secrets.GITHUB_TOKEN }} 76 | message: | 77 | **PR**: 78 | ``` 79 | ${{ needs.benchmark.outputs.PR-BENCH }} 80 | ``` 81 | **MAIN**: 82 | ``` 83 | ${{ needs.benchmark.outputs.MAIN-BENCH }} 84 | ``` 85 | 86 | - uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0 87 | with: 88 | labels: | 89 | benchmark 90 | github_token: ${{ secrets.GITHUB_TOKEN }} 91 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | - 'v*' 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | pull_request: 13 | paths-ignore: 14 | - 'docs/**' 15 | - '*.md' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | permissions: 23 | contents: write 24 | pull-requests: write 25 | uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 26 | with: 27 | license-check: true 28 | lint: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Vim swap files 133 | *.swp 134 | 135 | # macOS files 136 | .DS_Store 137 | 138 | # Clinic 139 | .clinic 140 | 141 | # lock files 142 | bun.lockb 143 | package-lock.json 144 | pnpm-lock.yaml 145 | yarn.lock 146 | 147 | # editor files 148 | .vscode 149 | .idea 150 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2018 Matteo Collina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmark/bench-cmp-branch.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { spawn } = require('child_process') 4 | 5 | const cliSelect = require('cli-select') 6 | const simpleGit = require('simple-git') 7 | 8 | const git = simpleGit(process.cwd()) 9 | 10 | const COMMAND = 'npm run bench' 11 | const DEFAULT_BRANCH = 'main' 12 | const PERCENT_THRESHOLD = 5 13 | const greyColor = '\x1b[30m' 14 | const redColor = '\x1b[31m' 15 | const greenColor = '\x1b[32m' 16 | const resetColor = '\x1b[0m' 17 | 18 | async function selectBranchName (message, branches) { 19 | console.log(message) 20 | const result = await cliSelect({ 21 | type: 'list', 22 | name: 'branch', 23 | values: branches 24 | }) 25 | console.log(result.value) 26 | return result.value 27 | } 28 | 29 | async function executeCommandOnBranch (command, branch) { 30 | console.log(`${greyColor}Checking out "${branch}"${resetColor}`) 31 | await git.checkout(branch) 32 | 33 | console.log(`${greyColor}Execute "${command}"${resetColor}`) 34 | const childProcess = spawn(command, { stdio: 'pipe', shell: true }) 35 | 36 | let result = '' 37 | childProcess.stdout.on('data', (data) => { 38 | process.stdout.write(data.toString()) 39 | result += data.toString() 40 | }) 41 | 42 | await new Promise(resolve => childProcess.on('close', resolve)) 43 | 44 | console.log() 45 | 46 | return parseBenchmarksStdout(result) 47 | } 48 | 49 | function parseBenchmarksStdout (text) { 50 | const results = [] 51 | 52 | const lines = text.split('\n') 53 | for (const line of lines) { 54 | const match = /^(.+?)(\.*) x (.+) ops\/sec .*$/.exec(line) 55 | if (match !== null) { 56 | results.push({ 57 | name: match[1], 58 | alignedName: match[1] + match[2], 59 | result: parseInt(match[3].split(',').join('')) 60 | }) 61 | } 62 | } 63 | 64 | return results 65 | } 66 | 67 | function compareResults (featureBranch, mainBranch) { 68 | for (const { name, alignedName, result: mainBranchResult } of mainBranch) { 69 | const featureBranchBenchmark = featureBranch.find(result => result.name === name) 70 | if (featureBranchBenchmark) { 71 | const featureBranchResult = featureBranchBenchmark.result 72 | const percent = (featureBranchResult - mainBranchResult) * 100 / mainBranchResult 73 | const roundedPercent = Math.round(percent * 100) / 100 74 | 75 | const percentString = roundedPercent > 0 ? `+${roundedPercent}%` : `${roundedPercent}%` 76 | const message = alignedName + percentString.padStart(7, '.') 77 | 78 | if (roundedPercent > PERCENT_THRESHOLD) { 79 | console.log(`${greenColor}${message}${resetColor}`) 80 | } else if (roundedPercent < -PERCENT_THRESHOLD) { 81 | console.log(`${redColor}${message}${resetColor}`) 82 | } else { 83 | console.log(message) 84 | } 85 | } 86 | } 87 | } 88 | 89 | (async function () { 90 | const branches = await git.branch() 91 | const currentBranch = branches.branches[branches.current] 92 | 93 | let featureBranch = null 94 | let mainBranch = null 95 | 96 | if (process.argv[2] === '--ci') { 97 | featureBranch = currentBranch.name 98 | mainBranch = DEFAULT_BRANCH 99 | } else { 100 | featureBranch = await selectBranchName('Select the branch you want to compare (feature branch):', branches.all) 101 | mainBranch = await selectBranchName('Select the branch you want to compare with (main branch):', branches.all) 102 | } 103 | 104 | try { 105 | const featureBranchResult = await executeCommandOnBranch(COMMAND, featureBranch) 106 | const mainBranchResult = await executeCommandOnBranch(COMMAND, mainBranch) 107 | compareResults(featureBranchResult, mainBranchResult) 108 | } catch (error) { 109 | console.error('Switch to origin branch due to an error', error.message) 110 | } 111 | 112 | await git.checkout(currentBranch.commit) 113 | await git.checkout(currentBranch.name) 114 | 115 | console.log(`${greyColor}Back to ${currentBranch.name} ${currentBranch.commit}${resetColor}`) 116 | })() 117 | -------------------------------------------------------------------------------- /benchmark/bench-cmp-lib.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const benchmark = require('benchmark') 4 | const suite = new benchmark.Suite() 5 | 6 | const STR_LEN = 1e4 7 | const LARGE_ARRAY_SIZE = 2e4 8 | const MULTI_ARRAY_LENGTH = 1e3 9 | 10 | const schema = { 11 | title: 'Example Schema', 12 | type: 'object', 13 | properties: { 14 | firstName: { 15 | type: 'string' 16 | }, 17 | lastName: { 18 | type: ['string', 'null'] 19 | }, 20 | age: { 21 | description: 'Age in years', 22 | type: 'integer', 23 | minimum: 0 24 | } 25 | } 26 | } 27 | const schemaCJS = { 28 | title: 'Example Schema', 29 | type: 'object', 30 | properties: { 31 | firstName: { 32 | type: 'string' 33 | }, 34 | lastName: { 35 | type: ['string', 'null'] 36 | }, 37 | age: { 38 | description: 'Age in years', 39 | type: 'number', 40 | minimum: 0 41 | } 42 | } 43 | } 44 | 45 | const schemaAJVJTD = { 46 | properties: { 47 | firstName: { 48 | type: 'string' 49 | }, 50 | lastName: { 51 | type: 'string', 52 | nullable: true 53 | }, 54 | age: { 55 | type: 'uint8' 56 | } 57 | } 58 | } 59 | 60 | const arraySchema = { 61 | title: 'array schema', 62 | type: 'array', 63 | items: schema 64 | } 65 | 66 | const arraySchemaCJS = { 67 | title: 'array schema', 68 | type: 'array', 69 | items: schemaCJS 70 | } 71 | 72 | const arraySchemaAJVJTD = { 73 | elements: schemaAJVJTD 74 | } 75 | 76 | const dateFormatSchema = { 77 | description: 'Date of birth', 78 | type: 'string', 79 | format: 'date' 80 | } 81 | 82 | const dateFormatSchemaCJS = { 83 | description: 'Date of birth', 84 | type: 'string', 85 | format: 'date' 86 | } 87 | 88 | const obj = { 89 | firstName: 'Matteo', 90 | lastName: 'Collina', 91 | age: 32 92 | } 93 | 94 | const date = new Date() 95 | 96 | const multiArray = new Array(MULTI_ARRAY_LENGTH) 97 | const largeArray = new Array(LARGE_ARRAY_SIZE) 98 | 99 | const CJS = require('compile-json-stringify') 100 | const CJSStringify = CJS(schemaCJS) 101 | const CJSStringifyArray = CJS(arraySchemaCJS) 102 | const CJSStringifyDate = CJS(dateFormatSchemaCJS) 103 | const CJSStringifyString = CJS({ type: 'string' }) 104 | 105 | const FJS = require('..') 106 | const stringify = FJS(schema) 107 | const stringifyArrayDefault = FJS(arraySchema) 108 | const stringifyArrayJSONStringify = FJS(arraySchema, { 109 | largeArrayMechanism: 'json-stringify' 110 | }) 111 | const stringifyDate = FJS(dateFormatSchema) 112 | const stringifyString = FJS({ type: 'string' }) 113 | let str = '' 114 | 115 | const Ajv = require('ajv/dist/jtd') 116 | const ajv = new Ajv() 117 | const ajvSerialize = ajv.compileSerializer(schemaAJVJTD) 118 | const ajvSerializeArray = ajv.compileSerializer(arraySchemaAJVJTD) 119 | const ajvSerializeString = ajv.compileSerializer({ type: 'string' }) 120 | 121 | const getRandomString = (length) => { 122 | if (!Number.isInteger(length)) { 123 | throw new Error('Expected integer length') 124 | } 125 | 126 | const validCharacters = 'abcdefghijklmnopqrstuvwxyz' 127 | const nValidCharacters = 26 128 | 129 | let result = '' 130 | for (let i = 0; i < length; ++i) { 131 | result += validCharacters[Math.floor(Math.random() * nValidCharacters)] 132 | } 133 | 134 | return result[0].toUpperCase() + result.slice(1) 135 | } 136 | 137 | for (let i = 0; i < STR_LEN; i++) { 138 | largeArray[i] = { 139 | firstName: getRandomString(8), 140 | lastName: getRandomString(6), 141 | age: Math.ceil(Math.random() * 99) 142 | } 143 | 144 | str += i 145 | if (i % 100 === 0) { 146 | str += '"' 147 | } 148 | } 149 | 150 | for (let i = STR_LEN; i < LARGE_ARRAY_SIZE; ++i) { 151 | largeArray[i] = { 152 | firstName: getRandomString(10), 153 | lastName: getRandomString(4), 154 | age: Math.ceil(Math.random() * 99) 155 | } 156 | } 157 | 158 | Number(str) 159 | 160 | for (let i = 0; i < MULTI_ARRAY_LENGTH; i++) { 161 | multiArray[i] = obj 162 | } 163 | 164 | suite.add('FJS creation', function () { 165 | FJS(schema) 166 | }) 167 | suite.add('CJS creation', function () { 168 | CJS(schemaCJS) 169 | }) 170 | suite.add('AJV Serialize creation', function () { 171 | ajv.compileSerializer(schemaAJVJTD) 172 | }) 173 | 174 | suite.add('JSON.stringify array', function () { 175 | JSON.stringify(multiArray) 176 | }) 177 | 178 | suite.add('fast-json-stringify array default', function () { 179 | stringifyArrayDefault(multiArray) 180 | }) 181 | 182 | suite.add('fast-json-stringify array json-stringify', function () { 183 | stringifyArrayJSONStringify(multiArray) 184 | }) 185 | 186 | suite.add('compile-json-stringify array', function () { 187 | CJSStringifyArray(multiArray) 188 | }) 189 | 190 | suite.add('AJV Serialize array', function () { 191 | ajvSerializeArray(multiArray) 192 | }) 193 | 194 | suite.add('JSON.stringify large array', function () { 195 | JSON.stringify(largeArray) 196 | }) 197 | 198 | suite.add('fast-json-stringify large array default', function () { 199 | stringifyArrayDefault(largeArray) 200 | }) 201 | 202 | suite.add('fast-json-stringify large array json-stringify', function () { 203 | stringifyArrayJSONStringify(largeArray) 204 | }) 205 | 206 | suite.add('compile-json-stringify large array', function () { 207 | CJSStringifyArray(largeArray) 208 | }) 209 | 210 | suite.add('AJV Serialize large array', function () { 211 | ajvSerializeArray(largeArray) 212 | }) 213 | 214 | suite.add('JSON.stringify long string', function () { 215 | JSON.stringify(str) 216 | }) 217 | 218 | suite.add('fast-json-stringify long string', function () { 219 | stringifyString(str) 220 | }) 221 | 222 | suite.add('compile-json-stringify long string', function () { 223 | CJSStringifyString(str) 224 | }) 225 | 226 | suite.add('AJV Serialize long string', function () { 227 | ajvSerializeString(str) 228 | }) 229 | 230 | suite.add('JSON.stringify short string', function () { 231 | JSON.stringify('hello world') 232 | }) 233 | 234 | suite.add('fast-json-stringify short string', function () { 235 | stringifyString('hello world') 236 | }) 237 | 238 | suite.add('compile-json-stringify short string', function () { 239 | CJSStringifyString('hello world') 240 | }) 241 | 242 | suite.add('AJV Serialize short string', function () { 243 | ajvSerializeString('hello world') 244 | }) 245 | 246 | suite.add('JSON.stringify obj', function () { 247 | JSON.stringify(obj) 248 | }) 249 | 250 | suite.add('fast-json-stringify obj', function () { 251 | stringify(obj) 252 | }) 253 | 254 | suite.add('compile-json-stringify obj', function () { 255 | CJSStringify(obj) 256 | }) 257 | 258 | suite.add('AJV Serialize obj', function () { 259 | ajvSerialize(obj) 260 | }) 261 | 262 | suite.add('JSON stringify date', function () { 263 | JSON.stringify(date) 264 | }) 265 | 266 | suite.add('fast-json-stringify date format', function () { 267 | stringifyDate(date) 268 | }) 269 | 270 | suite.add('compile-json-stringify date format', function () { 271 | CJSStringifyDate(date) 272 | }) 273 | 274 | suite.on('cycle', cycle) 275 | 276 | suite.run() 277 | 278 | function cycle (e) { 279 | console.log(e.target.toString()) 280 | } 281 | -------------------------------------------------------------------------------- /benchmark/bench-thread.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { workerData: benchmark, parentPort } = require('worker_threads') 4 | 5 | const Benchmark = require('benchmark') 6 | Benchmark.options.minSamples = 100 7 | 8 | const suite = Benchmark.Suite() 9 | 10 | const FJS = require('..') 11 | const stringify = FJS(benchmark.schema) 12 | 13 | suite 14 | .add(benchmark.name, () => { 15 | stringify(benchmark.input) 16 | }) 17 | .on('cycle', (event) => { 18 | parentPort.postMessage(String(event.target)) 19 | }) 20 | .on('complete', () => {}) 21 | .run() 22 | -------------------------------------------------------------------------------- /benchmark/bench.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const { Worker } = require('worker_threads') 5 | 6 | const BENCH_THREAD_PATH = path.join(__dirname, 'bench-thread.js') 7 | 8 | const LONG_STRING_LENGTH = 1e4 9 | const SHORT_ARRAY_SIZE = 1e3 10 | 11 | const shortArrayOfNumbers = new Array(SHORT_ARRAY_SIZE) 12 | const shortArrayOfIntegers = new Array(SHORT_ARRAY_SIZE) 13 | const shortArrayOfShortStrings = new Array(SHORT_ARRAY_SIZE) 14 | const shortArrayOfLongStrings = new Array(SHORT_ARRAY_SIZE) 15 | const shortArrayOfMultiObject = new Array(SHORT_ARRAY_SIZE) 16 | 17 | function getRandomInt (max) { 18 | return Math.floor(Math.random() * max) 19 | } 20 | 21 | let longSimpleString = '' 22 | for (let i = 0; i < LONG_STRING_LENGTH; i++) { 23 | longSimpleString += i 24 | } 25 | 26 | let longString = '' 27 | for (let i = 0; i < LONG_STRING_LENGTH; i++) { 28 | longString += i 29 | if (i % 100 === 0) { 30 | longString += '"' 31 | } 32 | } 33 | 34 | for (let i = 0; i < SHORT_ARRAY_SIZE; i++) { 35 | shortArrayOfNumbers[i] = getRandomInt(1000) 36 | shortArrayOfIntegers[i] = getRandomInt(1000) 37 | shortArrayOfShortStrings[i] = 'hello world' 38 | shortArrayOfLongStrings[i] = longString 39 | shortArrayOfMultiObject[i] = { s: 'hello world', n: 42, b: true } 40 | } 41 | 42 | const benchmarks = [ 43 | { 44 | name: 'short string', 45 | schema: { 46 | type: 'string' 47 | }, 48 | input: 'hello world' 49 | }, 50 | { 51 | name: 'unsafe short string', 52 | schema: { 53 | type: 'string', 54 | format: 'unsafe' 55 | }, 56 | input: 'hello world' 57 | }, 58 | { 59 | name: 'short string with double quote', 60 | schema: { 61 | type: 'string' 62 | }, 63 | input: 'hello " world' 64 | }, 65 | { 66 | name: 'long string without double quotes', 67 | schema: { 68 | type: 'string' 69 | }, 70 | input: longSimpleString 71 | }, 72 | { 73 | name: 'unsafe long string without double quotes', 74 | schema: { 75 | type: 'string', 76 | format: 'unsafe' 77 | }, 78 | input: longSimpleString 79 | }, 80 | { 81 | name: 'long string', 82 | schema: { 83 | type: 'string' 84 | }, 85 | input: longString 86 | }, 87 | { 88 | name: 'unsafe long string', 89 | schema: { 90 | type: 'string', 91 | format: 'unsafe' 92 | }, 93 | input: longString 94 | }, 95 | { 96 | name: 'number', 97 | schema: { 98 | type: 'number' 99 | }, 100 | input: 42 101 | }, 102 | { 103 | name: 'integer', 104 | schema: { 105 | type: 'integer' 106 | }, 107 | input: 42 108 | }, 109 | { 110 | name: 'formatted date-time', 111 | schema: { 112 | type: 'string', 113 | format: 'date-time' 114 | }, 115 | input: new Date() 116 | }, 117 | { 118 | name: 'formatted date', 119 | schema: { 120 | type: 'string', 121 | format: 'date' 122 | }, 123 | input: new Date() 124 | }, 125 | { 126 | name: 'formatted time', 127 | schema: { 128 | type: 'string', 129 | format: 'time' 130 | }, 131 | input: new Date() 132 | }, 133 | { 134 | name: 'short array of numbers', 135 | schema: { 136 | type: 'array', 137 | items: { type: 'number' } 138 | }, 139 | input: shortArrayOfNumbers 140 | }, 141 | { 142 | name: 'short array of integers', 143 | schema: { 144 | type: 'array', 145 | items: { type: 'integer' } 146 | }, 147 | input: shortArrayOfIntegers 148 | }, 149 | { 150 | name: 'short array of short strings', 151 | schema: { 152 | type: 'array', 153 | items: { type: 'string' } 154 | }, 155 | input: shortArrayOfShortStrings 156 | }, 157 | { 158 | name: 'short array of long strings', 159 | schema: { 160 | type: 'array', 161 | items: { type: 'string' } 162 | }, 163 | input: shortArrayOfShortStrings 164 | }, 165 | { 166 | name: 'short array of objects with properties of different types', 167 | schema: { 168 | type: 'array', 169 | items: { 170 | type: 'object', 171 | properties: { 172 | s: { type: 'string' }, 173 | n: { type: 'number' }, 174 | b: { type: 'boolean' } 175 | } 176 | } 177 | }, 178 | input: shortArrayOfMultiObject 179 | }, 180 | { 181 | name: 'object with number property', 182 | schema: { 183 | type: 'object', 184 | properties: { 185 | a: { type: 'number' } 186 | } 187 | }, 188 | input: { a: 42 } 189 | }, 190 | { 191 | name: 'object with integer property', 192 | schema: { 193 | type: 'object', 194 | properties: { 195 | a: { type: 'integer' } 196 | } 197 | }, 198 | input: { a: 42 } 199 | }, 200 | { 201 | name: 'object with short string property', 202 | schema: { 203 | type: 'object', 204 | properties: { 205 | a: { type: 'string' } 206 | } 207 | }, 208 | input: { a: 'hello world' } 209 | }, 210 | { 211 | name: 'object with long string property', 212 | schema: { 213 | type: 'object', 214 | properties: { 215 | a: { type: 'string' } 216 | } 217 | }, 218 | input: { a: longString } 219 | }, 220 | { 221 | name: 'object with properties of different types', 222 | schema: { 223 | type: 'object', 224 | properties: { 225 | s1: { type: 'string' }, 226 | n1: { type: 'number' }, 227 | b1: { type: 'boolean' }, 228 | s2: { type: 'string' }, 229 | n2: { type: 'number' }, 230 | b2: { type: 'boolean' }, 231 | s3: { type: 'string' }, 232 | n3: { type: 'number' }, 233 | b3: { type: 'boolean' }, 234 | s4: { type: 'string' }, 235 | n4: { type: 'number' }, 236 | b4: { type: 'boolean' }, 237 | s5: { type: 'string' }, 238 | n5: { type: 'number' }, 239 | b5: { type: 'boolean' } 240 | } 241 | }, 242 | input: { 243 | s1: 'hello world', 244 | n1: 42, 245 | b1: true, 246 | s2: 'hello world', 247 | n2: 42, 248 | b2: true, 249 | s3: 'hello world', 250 | n3: 42, 251 | b3: true, 252 | s4: 'hello world', 253 | n4: 42, 254 | b4: true, 255 | s5: 'hello world', 256 | n5: 42, 257 | b5: true 258 | } 259 | }, 260 | { 261 | name: 'simple object', 262 | schema: { 263 | title: 'Example Schema', 264 | type: 'object', 265 | properties: { 266 | firstName: { 267 | type: 'string' 268 | }, 269 | lastName: { 270 | type: ['string', 'null'] 271 | }, 272 | age: { 273 | description: 'Age in years', 274 | type: 'integer', 275 | minimum: 0 276 | } 277 | } 278 | }, 279 | input: { firstName: 'Max', lastName: 'Power', age: 22 } 280 | }, 281 | { 282 | name: 'simple object with required fields', 283 | schema: { 284 | title: 'Example Schema', 285 | type: 'object', 286 | properties: { 287 | firstName: { 288 | type: 'string' 289 | }, 290 | lastName: { 291 | type: ['string', 'null'] 292 | }, 293 | age: { 294 | description: 'Age in years', 295 | type: 'integer', 296 | minimum: 0 297 | } 298 | }, 299 | required: ['firstName', 'lastName', 'age'] 300 | }, 301 | input: { firstName: 'Max', lastName: 'Power', age: 22 } 302 | }, 303 | { 304 | name: 'object with const string property', 305 | schema: { 306 | type: 'object', 307 | properties: { 308 | a: { const: 'const string' } 309 | } 310 | }, 311 | input: { a: 'const string' } 312 | }, 313 | { 314 | name: 'object with const number property', 315 | schema: { 316 | type: 'object', 317 | properties: { 318 | a: { const: 1 } 319 | } 320 | }, 321 | input: { a: 1 } 322 | }, 323 | { 324 | name: 'object with const bool property', 325 | schema: { 326 | type: 'object', 327 | properties: { 328 | a: { const: true } 329 | } 330 | }, 331 | input: { a: true } 332 | }, 333 | { 334 | name: 'object with const object property', 335 | schema: { 336 | type: 'object', 337 | properties: { 338 | foo: { const: { bar: 'baz' } } 339 | } 340 | }, 341 | input: { 342 | foo: { bar: 'baz' } 343 | } 344 | }, 345 | { 346 | name: 'object with const null property', 347 | schema: { 348 | type: 'object', 349 | properties: { 350 | foo: { const: null } 351 | } 352 | }, 353 | input: { 354 | foo: null 355 | } 356 | } 357 | ] 358 | 359 | async function runBenchmark (benchmark) { 360 | const worker = new Worker(BENCH_THREAD_PATH, { workerData: benchmark }) 361 | 362 | return new Promise((resolve, reject) => { 363 | let result = null 364 | worker.on('error', reject) 365 | worker.on('message', (benchResult) => { 366 | result = benchResult 367 | }) 368 | worker.on('exit', (code) => { 369 | if (code === 0) { 370 | resolve(result) 371 | } else { 372 | reject(new Error(`Worker stopped with exit code ${code}`)) 373 | } 374 | }) 375 | }) 376 | } 377 | 378 | async function runBenchmarks () { 379 | let maxNameLength = 0 380 | for (const benchmark of benchmarks) { 381 | maxNameLength = Math.max(benchmark.name.length, maxNameLength) 382 | } 383 | 384 | for (const benchmark of benchmarks) { 385 | benchmark.name = benchmark.name.padEnd(maxNameLength, '.') 386 | const resultMessage = await runBenchmark(benchmark) 387 | console.log(resultMessage) 388 | } 389 | } 390 | 391 | runBenchmarks() 392 | -------------------------------------------------------------------------------- /build/build-schema-validator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Ajv = require('ajv') 4 | const standaloneCode = require('ajv/dist/standalone').default 5 | const ajvFormats = require('ajv-formats') 6 | const fs = require('fs') 7 | const path = require('path') 8 | 9 | const ajv = new Ajv({ 10 | addUsedSchema: false, 11 | allowUnionTypes: true, 12 | code: { 13 | source: true, 14 | lines: true, 15 | optimize: 3 16 | } 17 | }) 18 | ajvFormats(ajv) 19 | 20 | const schema = require('ajv/lib/refs/json-schema-draft-07.json') 21 | const validate = ajv.compile(schema) 22 | const validationCode = standaloneCode(ajv, validate) 23 | 24 | const moduleCode = `/* CODE GENERATED BY '${path.basename(__filename)}' DO NOT EDIT! */\n${validationCode}` 25 | 26 | fs.writeFileSync(path.join(__dirname, '../lib/schema-validator.js'), moduleCode) 27 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: [ 5 | ...require('neostandard').resolveIgnoresFromGitignore(), 6 | 'lib/schema-validator.js' 7 | ], 8 | ts: true 9 | }) 10 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fastJson = require('..') 4 | const stringify = fastJson({ 5 | title: 'Example Schema', 6 | type: 'object', 7 | properties: { 8 | firstName: { 9 | type: 'string' 10 | }, 11 | lastName: { 12 | type: 'string' 13 | }, 14 | age: { 15 | description: 'Age in years', 16 | type: 'integer' 17 | }, 18 | now: { 19 | type: 'string' 20 | }, 21 | birthdate: { 22 | type: ['string'], 23 | format: 'date-time' 24 | }, 25 | reg: { 26 | type: 'string' 27 | }, 28 | obj: { 29 | type: 'object', 30 | properties: { 31 | bool: { 32 | type: 'boolean' 33 | } 34 | } 35 | }, 36 | arr: { 37 | type: 'array', 38 | items: { 39 | type: 'object', 40 | properties: { 41 | str: { 42 | type: 'string' 43 | } 44 | } 45 | } 46 | } 47 | }, 48 | required: ['now'], 49 | patternProperties: { 50 | '.*foo$': { 51 | type: 'string' 52 | }, 53 | test: { 54 | type: 'number' 55 | }, 56 | date: { 57 | type: 'string', 58 | format: 'date-time' 59 | } 60 | }, 61 | additionalProperties: { 62 | type: 'string' 63 | } 64 | }) 65 | 66 | console.log(stringify({ 67 | firstName: 'Matteo', 68 | lastName: 'Collina', 69 | age: 32, 70 | now: new Date(), 71 | reg: /"([^"]|\\")*"/, 72 | foo: 'hello', 73 | numfoo: 42, 74 | test: 42, 75 | strtest: '23', 76 | arr: [{ str: 'stark' }, { str: 'lannister' }], 77 | obj: { bool: true }, 78 | notmatch: 'valar morghulis', 79 | notmatchobj: { a: true }, 80 | notmatchnum: 42 81 | })) 82 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | 5 | const stringify = require('fast-json-stringify')({ 6 | type: 'object', 7 | properties: { 8 | hello: { 9 | type: 'string' 10 | }, 11 | data: { 12 | type: 'number' 13 | }, 14 | nested: { 15 | type: 'object', 16 | properties: { 17 | more: { 18 | type: 'string' 19 | } 20 | } 21 | } 22 | } 23 | }) 24 | 25 | const server = http.createServer(handle) 26 | 27 | function handle (req, res) { 28 | const data = { 29 | hello: 'world', 30 | data: 42, 31 | nested: { 32 | more: 'data' 33 | } 34 | } 35 | if (req.url === '/JSON') { 36 | res.end(JSON.stringify(data)) 37 | } else { 38 | res.end(stringify(data)) 39 | } 40 | } 41 | 42 | server.listen(3000) 43 | -------------------------------------------------------------------------------- /lib/location.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class Location { 4 | constructor (schema, schemaId, jsonPointer = '#') { 5 | this.schema = schema 6 | this.schemaId = schemaId 7 | this.jsonPointer = jsonPointer 8 | } 9 | 10 | getPropertyLocation (propertyName) { 11 | const propertyLocation = new Location( 12 | this.schema[propertyName], 13 | this.schemaId, 14 | this.jsonPointer + '/' + propertyName 15 | ) 16 | return propertyLocation 17 | } 18 | 19 | getSchemaRef () { 20 | return this.schemaId + this.jsonPointer 21 | } 22 | } 23 | 24 | module.exports = Location 25 | -------------------------------------------------------------------------------- /lib/merge-schemas.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { mergeSchemas: _mergeSchemas } = require('@fastify/merge-json-schemas') 4 | 5 | function mergeSchemas (schemas) { 6 | return _mergeSchemas(schemas, { onConflict: 'skip' }) 7 | } 8 | 9 | module.exports = mergeSchemas 10 | -------------------------------------------------------------------------------- /lib/serializer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // eslint-disable-next-line 4 | const STR_ESCAPE = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/ 5 | 6 | module.exports = class Serializer { 7 | constructor (options) { 8 | switch (options && options.rounding) { 9 | case 'floor': 10 | this.parseInteger = Math.floor 11 | break 12 | case 'ceil': 13 | this.parseInteger = Math.ceil 14 | break 15 | case 'round': 16 | this.parseInteger = Math.round 17 | break 18 | case 'trunc': 19 | default: 20 | this.parseInteger = Math.trunc 21 | break 22 | } 23 | this._options = options 24 | } 25 | 26 | asInteger (i) { 27 | if (Number.isInteger(i)) { 28 | return '' + i 29 | } else if (typeof i === 'bigint') { 30 | return i.toString() 31 | } 32 | /* eslint no-undef: "off" */ 33 | const integer = this.parseInteger(i) 34 | // check if number is Infinity or NaN 35 | // eslint-disable-next-line no-self-compare 36 | if (integer === Infinity || integer === -Infinity || integer !== integer) { 37 | throw new Error(`The value "${i}" cannot be converted to an integer.`) 38 | } 39 | return '' + integer 40 | } 41 | 42 | asNumber (i) { 43 | // fast cast to number 44 | const num = Number(i) 45 | // check if number is NaN 46 | // eslint-disable-next-line no-self-compare 47 | if (num !== num) { 48 | throw new Error(`The value "${i}" cannot be converted to a number.`) 49 | } else if (num === Infinity || num === -Infinity) { 50 | return 'null' 51 | } else { 52 | return '' + num 53 | } 54 | } 55 | 56 | asBoolean (bool) { 57 | return bool && 'true' || 'false' // eslint-disable-line 58 | } 59 | 60 | asDateTime (date) { 61 | if (date === null) return '""' 62 | if (date instanceof Date) { 63 | return '"' + date.toISOString() + '"' 64 | } 65 | if (typeof date === 'string') { 66 | return '"' + date + '"' 67 | } 68 | throw new Error(`The value "${date}" cannot be converted to a date-time.`) 69 | } 70 | 71 | asDate (date) { 72 | if (date === null) return '""' 73 | if (date instanceof Date) { 74 | return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + '"' 75 | } 76 | if (typeof date === 'string') { 77 | return '"' + date + '"' 78 | } 79 | throw new Error(`The value "${date}" cannot be converted to a date.`) 80 | } 81 | 82 | asTime (date) { 83 | if (date === null) return '""' 84 | if (date instanceof Date) { 85 | return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + '"' 86 | } 87 | if (typeof date === 'string') { 88 | return '"' + date + '"' 89 | } 90 | throw new Error(`The value "${date}" cannot be converted to a time.`) 91 | } 92 | 93 | asString (str) { 94 | const len = str.length 95 | if (len < 42) { 96 | // magically escape strings for json 97 | // relying on their charCodeAt 98 | // everything below 32 needs JSON.stringify() 99 | // every string that contain surrogate needs JSON.stringify() 100 | // 34 and 92 happens all the time, so we 101 | // have a fast case for them 102 | let result = '' 103 | let last = -1 104 | let point = 255 105 | for (let i = 0; i < len; i++) { 106 | point = str.charCodeAt(i) 107 | if ( 108 | point === 0x22 || // '"' 109 | point === 0x5c // '\' 110 | ) { 111 | last === -1 && (last = 0) 112 | result += str.slice(last, i) + '\\' 113 | last = i 114 | } else if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) { 115 | // The current character is non-printable characters or a surrogate. 116 | return JSON.stringify(str) 117 | } 118 | } 119 | return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"') 120 | } else if (len < 5000 && STR_ESCAPE.test(str) === false) { 121 | // Only use the regular expression for shorter input. The overhead is otherwise too much. 122 | return '"' + str + '"' 123 | } else { 124 | return JSON.stringify(str) 125 | } 126 | } 127 | 128 | asUnsafeString (str) { 129 | return '"' + str + '"' 130 | } 131 | 132 | getState () { 133 | return this._options 134 | } 135 | 136 | static restoreFromState (state) { 137 | return new Serializer(state) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/standalone.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function buildStandaloneCode (contextFunc, context, serializer, validator) { 4 | let ajvDependencyCode = '' 5 | if (context.validatorSchemasIds.size > 0) { 6 | ajvDependencyCode += 'const Validator = require(\'fast-json-stringify/lib/validator\')\n' 7 | ajvDependencyCode += `const validatorState = ${JSON.stringify(validator.getState())}\n` 8 | ajvDependencyCode += 'const validator = Validator.restoreFromState(validatorState)\n' 9 | } else { 10 | ajvDependencyCode += 'const validator = null\n' 11 | } 12 | 13 | // Don't need to keep external schemas once compiled 14 | // validatorState will hold external schemas if it needs them 15 | const { schema, ...serializerState } = serializer.getState() 16 | 17 | return ` 18 | 'use strict' 19 | 20 | const Serializer = require('fast-json-stringify/lib/serializer') 21 | const serializerState = ${JSON.stringify(serializerState)} 22 | const serializer = Serializer.restoreFromState(serializerState) 23 | 24 | ${ajvDependencyCode} 25 | 26 | module.exports = ${contextFunc.toString()}(validator, serializer)` 27 | } 28 | 29 | module.exports = buildStandaloneCode 30 | 31 | module.exports.dependencies = { 32 | Serializer: require('./serializer'), 33 | Validator: require('./validator') 34 | } 35 | -------------------------------------------------------------------------------- /lib/validator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Ajv = require('ajv') 4 | const fastUri = require('fast-uri') 5 | const ajvFormats = require('ajv-formats') 6 | const clone = require('rfdc')({ proto: true }) 7 | 8 | class Validator { 9 | constructor (ajvOptions) { 10 | this.ajv = new Ajv({ 11 | ...ajvOptions, 12 | strictSchema: false, 13 | validateSchema: false, 14 | allowUnionTypes: true, 15 | uriResolver: fastUri 16 | }) 17 | 18 | ajvFormats(this.ajv) 19 | 20 | this.ajv.addKeyword({ 21 | keyword: 'fjs_type', 22 | type: 'object', 23 | errors: false, 24 | validate: (_type, date) => { 25 | return date instanceof Date 26 | } 27 | }) 28 | 29 | this._ajvSchemas = {} 30 | this._ajvOptions = ajvOptions || {} 31 | } 32 | 33 | addSchema (schema, schemaName) { 34 | let schemaKey = schema.$id || schemaName 35 | if (schema.$id !== undefined && schema.$id[0] === '#') { 36 | schemaKey = schemaName + schema.$id // relative URI 37 | } 38 | 39 | if ( 40 | this.ajv.refs[schemaKey] === undefined && 41 | this.ajv.schemas[schemaKey] === undefined 42 | ) { 43 | const ajvSchema = clone(schema) 44 | this.convertSchemaToAjvFormat(ajvSchema) 45 | this.ajv.addSchema(ajvSchema, schemaKey) 46 | this._ajvSchemas[schemaKey] = schema 47 | } 48 | } 49 | 50 | validate (schemaRef, data) { 51 | return this.ajv.validate(schemaRef, data) 52 | } 53 | 54 | // Ajv does not support js date format. In order to properly validate objects containing a date, 55 | // it needs to replace all occurrences of the string date format with a custom keyword fjs_type. 56 | // (see https://github.com/fastify/fast-json-stringify/pull/441) 57 | convertSchemaToAjvFormat (schema) { 58 | if (schema === null) return 59 | 60 | if (schema.type === 'string') { 61 | schema.fjs_type = 'string' 62 | schema.type = ['string', 'object'] 63 | } else if ( 64 | Array.isArray(schema.type) && 65 | schema.type.includes('string') && 66 | !schema.type.includes('object') 67 | ) { 68 | schema.fjs_type = 'string' 69 | schema.type.push('object') 70 | } 71 | for (const property in schema) { 72 | if (typeof schema[property] === 'object') { 73 | this.convertSchemaToAjvFormat(schema[property]) 74 | } 75 | } 76 | } 77 | 78 | getState () { 79 | return { 80 | ajvOptions: this._ajvOptions, 81 | ajvSchemas: this._ajvSchemas 82 | } 83 | } 84 | 85 | static restoreFromState (state) { 86 | const validator = new Validator(state.ajvOptions) 87 | for (const [id, ajvSchema] of Object.entries(state.ajvSchemas)) { 88 | validator.ajv.addSchema(ajvSchema, id) 89 | } 90 | return validator 91 | } 92 | } 93 | 94 | module.exports = Validator 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-json-stringify", 3 | "version": "6.0.1", 4 | "description": "Stringify your JSON at max speed", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "bench": "node ./benchmark/bench.js", 10 | "bench:cmp": "node ./benchmark/bench-cmp-branch.js", 11 | "bench:cmp:ci": "node ./benchmark/bench-cmp-branch.js --ci", 12 | "benchmark": "node ./benchmark/bench-cmp-lib.js", 13 | "lint": "eslint", 14 | "lint:fix": "eslint --fix", 15 | "test:typescript": "tsd", 16 | "test:unit": "c8 node --test", 17 | "test": "npm run test:unit && npm run test:typescript" 18 | }, 19 | "precommit": [ 20 | "lint", 21 | "test" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/fastify/fast-json-stringify.git" 26 | }, 27 | "keywords": [ 28 | "json", 29 | "stringify", 30 | "schema", 31 | "fast" 32 | ], 33 | "author": "Matteo Collina ", 34 | "contributors": [ 35 | { 36 | "name": "Tomas Della Vedova", 37 | "url": "http://delved.org" 38 | }, 39 | { 40 | "name": "Aras Abbasi", 41 | "email": "aras.abbasi@gmail.com" 42 | }, 43 | { 44 | "name": "Manuel Spigolon", 45 | "email": "behemoth89@gmail.com" 46 | }, 47 | { 48 | "name": "Frazer Smith", 49 | "email": "frazer.dev@icloud.com", 50 | "url": "https://github.com/fdawgs" 51 | } 52 | ], 53 | "license": "MIT", 54 | "bugs": { 55 | "url": "https://github.com/fastify/fast-json-stringify/issues" 56 | }, 57 | "homepage": "https://github.com/fastify/fast-json-stringify#readme", 58 | "funding": [ 59 | { 60 | "type": "github", 61 | "url": "https://github.com/sponsors/fastify" 62 | }, 63 | { 64 | "type": "opencollective", 65 | "url": "https://opencollective.com/fastify" 66 | } 67 | ], 68 | "devDependencies": { 69 | "@fastify/pre-commit": "^2.1.0", 70 | "@sinclair/typebox": "^0.34.3", 71 | "benchmark": "^2.1.4", 72 | "c8": "^10.1.2", 73 | "cli-select": "^1.1.2", 74 | "compile-json-stringify": "^0.1.2", 75 | "eslint": "^9.17.0", 76 | "fast-json-stringify": ".", 77 | "is-my-json-valid": "^2.20.6", 78 | "neostandard": "^0.12.0", 79 | "simple-git": "^3.23.0", 80 | "tsd": "^0.32.0", 81 | "webpack": "^5.90.3" 82 | }, 83 | "dependencies": { 84 | "@fastify/merge-json-schemas": "^0.2.0", 85 | "ajv": "^8.12.0", 86 | "ajv-formats": "^3.0.1", 87 | "fast-uri": "^3.0.0", 88 | "json-schema-ref-resolver": "^3.0.0", 89 | "rfdc": "^1.2.0" 90 | }, 91 | "runkitExampleFilename": "./examples/example.js" 92 | } 93 | -------------------------------------------------------------------------------- /test/additionalProperties.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('additionalProperties', (t) => { 7 | t.plan(1) 8 | const stringify = build({ 9 | title: 'additionalProperties', 10 | type: 'object', 11 | properties: { 12 | str: { 13 | type: 'string' 14 | } 15 | }, 16 | additionalProperties: { 17 | type: 'string' 18 | } 19 | }) 20 | 21 | const obj = { str: 'test', foo: 42, ofoo: true, foof: 'string', objfoo: { a: true } } 22 | t.assert.equal(stringify(obj), '{"str":"test","foo":"42","ofoo":"true","foof":"string","objfoo":"[object Object]"}') 23 | }) 24 | 25 | test('additionalProperties should not change properties', (t) => { 26 | t.plan(1) 27 | const stringify = build({ 28 | title: 'patternProperties should not change properties', 29 | type: 'object', 30 | properties: { 31 | foo: { 32 | type: 'string' 33 | } 34 | }, 35 | additionalProperties: { 36 | type: 'number' 37 | } 38 | }) 39 | 40 | const obj = { foo: '42', ofoo: 42 } 41 | t.assert.equal(stringify(obj), '{"foo":"42","ofoo":42}') 42 | }) 43 | 44 | test('additionalProperties should not change properties and patternProperties', (t) => { 45 | t.plan(1) 46 | const stringify = build({ 47 | title: 'patternProperties should not change properties', 48 | type: 'object', 49 | properties: { 50 | foo: { 51 | type: 'string' 52 | } 53 | }, 54 | patternProperties: { 55 | foo: { 56 | type: 'string' 57 | } 58 | }, 59 | additionalProperties: { 60 | type: 'number' 61 | } 62 | }) 63 | 64 | const obj = { foo: '42', ofoo: 42, test: '42' } 65 | t.assert.equal(stringify(obj), '{"foo":"42","ofoo":"42","test":42}') 66 | }) 67 | 68 | test('additionalProperties set to true, use of fast-safe-stringify', (t) => { 69 | t.plan(1) 70 | const stringify = build({ 71 | title: 'check string coerce', 72 | type: 'object', 73 | properties: {}, 74 | additionalProperties: true 75 | }) 76 | 77 | const obj = { foo: true, ofoo: 42, arrfoo: ['array', 'test'], objfoo: { a: 'world' } } 78 | t.assert.equal(stringify(obj), '{"foo":true,"ofoo":42,"arrfoo":["array","test"],"objfoo":{"a":"world"}}') 79 | }) 80 | 81 | test('additionalProperties - string coerce', (t) => { 82 | t.plan(1) 83 | const stringify = build({ 84 | title: 'check string coerce', 85 | type: 'object', 86 | properties: {}, 87 | additionalProperties: { 88 | type: 'string' 89 | } 90 | }) 91 | 92 | const obj = { foo: true, ofoo: 42, arrfoo: ['array', 'test'], objfoo: { a: 'world' } } 93 | t.assert.equal(stringify(obj), '{"foo":"true","ofoo":"42","arrfoo":"array,test","objfoo":"[object Object]"}') 94 | }) 95 | 96 | test('additionalProperties - number skip', (t) => { 97 | t.plan(1) 98 | const stringify = build({ 99 | title: 'check number coerce', 100 | type: 'object', 101 | properties: {}, 102 | additionalProperties: { 103 | type: 'number' 104 | } 105 | }) 106 | 107 | // const obj = { foo: true, ofoo: '42', xfoo: 'string', arrfoo: [1, 2], objfoo: { num: 42 } } 108 | const obj = { foo: true, ofoo: '42' } 109 | t.assert.equal(stringify(obj), '{"foo":1,"ofoo":42}') 110 | }) 111 | 112 | test('additionalProperties - boolean coerce', (t) => { 113 | t.plan(1) 114 | const stringify = build({ 115 | title: 'check boolean coerce', 116 | type: 'object', 117 | properties: {}, 118 | additionalProperties: { 119 | type: 'boolean' 120 | } 121 | }) 122 | 123 | const obj = { foo: 'true', ofoo: 0, arrfoo: [1, 2], objfoo: { a: true } } 124 | t.assert.equal(stringify(obj), '{"foo":true,"ofoo":false,"arrfoo":true,"objfoo":true}') 125 | }) 126 | 127 | test('additionalProperties - object coerce', (t) => { 128 | t.plan(1) 129 | const stringify = build({ 130 | title: 'check object coerce', 131 | type: 'object', 132 | properties: {}, 133 | additionalProperties: { 134 | type: 'object', 135 | properties: { 136 | answer: { 137 | type: 'number' 138 | } 139 | } 140 | } 141 | }) 142 | 143 | const obj = { objfoo: { answer: 42 } } 144 | t.assert.equal(stringify(obj), '{"objfoo":{"answer":42}}') 145 | }) 146 | 147 | test('additionalProperties - array coerce', (t) => { 148 | t.plan(2) 149 | const stringify = build({ 150 | title: 'check array coerce', 151 | type: 'object', 152 | properties: {}, 153 | additionalProperties: { 154 | type: 'array', 155 | items: { 156 | type: 'string' 157 | } 158 | } 159 | }) 160 | 161 | const coercibleValues = { arrfoo: [1, 2] } 162 | t.assert.equal(stringify(coercibleValues), '{"arrfoo":["1","2"]}') 163 | 164 | const incoercibleValues = { foo: 'true', ofoo: 0, objfoo: { tyrion: 'lannister' } } 165 | t.assert.throws(() => stringify(incoercibleValues)) 166 | }) 167 | 168 | test('additionalProperties with empty schema', (t) => { 169 | t.plan(1) 170 | const stringify = build({ 171 | type: 'object', 172 | additionalProperties: {} 173 | }) 174 | 175 | const obj = { a: 1, b: true, c: null } 176 | t.assert.equal(stringify(obj), '{"a":1,"b":true,"c":null}') 177 | }) 178 | 179 | test('additionalProperties with nested empty schema', (t) => { 180 | t.plan(1) 181 | const stringify = build({ 182 | type: 'object', 183 | properties: { 184 | data: { type: 'object', additionalProperties: {} } 185 | }, 186 | required: ['data'] 187 | }) 188 | 189 | const obj = { data: { a: 1, b: true, c: null } } 190 | t.assert.equal(stringify(obj), '{"data":{"a":1,"b":true,"c":null}}') 191 | }) 192 | 193 | test('nested additionalProperties', (t) => { 194 | t.plan(1) 195 | const stringify = build({ 196 | title: 'additionalProperties', 197 | type: 'array', 198 | items: { 199 | type: 'object', 200 | properties: { 201 | ap: { 202 | type: 'object', 203 | additionalProperties: { type: 'string' } 204 | } 205 | } 206 | } 207 | }) 208 | 209 | const obj = [{ ap: { value: 'string' } }] 210 | t.assert.equal(stringify(obj), '[{"ap":{"value":"string"}}]') 211 | }) 212 | 213 | test('very nested additionalProperties', (t) => { 214 | t.plan(1) 215 | const stringify = build({ 216 | title: 'additionalProperties', 217 | type: 'array', 218 | items: { 219 | type: 'object', 220 | properties: { 221 | ap: { 222 | type: 'object', 223 | properties: { 224 | nested: { 225 | type: 'object', 226 | properties: { 227 | moarNested: { 228 | type: 'object', 229 | properties: { 230 | finally: { 231 | type: 'object', 232 | additionalProperties: { 233 | type: 'string' 234 | } 235 | } 236 | } 237 | } 238 | } 239 | } 240 | } 241 | } 242 | } 243 | } 244 | }) 245 | 246 | const obj = [{ ap: { nested: { moarNested: { finally: { value: 'str' } } } } }] 247 | t.assert.equal(stringify(obj), '[{"ap":{"nested":{"moarNested":{"finally":{"value":"str"}}}}}]') 248 | }) 249 | 250 | test('nested additionalProperties set to true', (t) => { 251 | t.plan(1) 252 | const stringify = build({ 253 | title: 'nested additionalProperties=true', 254 | type: 'object', 255 | properties: { 256 | ap: { 257 | type: 'object', 258 | additionalProperties: true 259 | } 260 | } 261 | }) 262 | 263 | const obj = { ap: { value: 'string', someNumber: 42 } } 264 | t.assert.equal(stringify(obj), '{"ap":{"value":"string","someNumber":42}}') 265 | }) 266 | 267 | test('field passed to fastSafeStringify as undefined should be removed', (t) => { 268 | t.plan(1) 269 | const stringify = build({ 270 | title: 'nested additionalProperties=true', 271 | type: 'object', 272 | properties: { 273 | ap: { 274 | type: 'object', 275 | additionalProperties: true 276 | } 277 | } 278 | }) 279 | 280 | const obj = { ap: { value: 'string', someNumber: undefined } } 281 | t.assert.equal(stringify(obj), '{"ap":{"value":"string"}}') 282 | }) 283 | 284 | test('property without type but with enum, will acts as additionalProperties', (t) => { 285 | t.plan(1) 286 | const stringify = build({ 287 | title: 'automatic additionalProperties', 288 | type: 'object', 289 | properties: { 290 | ap: { 291 | enum: ['foobar', 42, ['foo', 'bar'], {}] 292 | } 293 | } 294 | }) 295 | 296 | const obj = { ap: { additional: 'field' } } 297 | t.assert.equal(stringify(obj), '{"ap":{"additional":"field"}}') 298 | }) 299 | 300 | test('property without type but with enum, will acts as additionalProperties without overwriting', (t) => { 301 | t.plan(1) 302 | const stringify = build({ 303 | title: 'automatic additionalProperties', 304 | type: 'object', 305 | properties: { 306 | ap: { 307 | additionalProperties: false, 308 | enum: ['foobar', 42, ['foo', 'bar'], {}] 309 | } 310 | } 311 | }) 312 | 313 | const obj = { ap: { additional: 'field' } } 314 | t.assert.equal(stringify(obj), '{"ap":{}}') 315 | }) 316 | 317 | test('function and symbol references are not serialized as undefined', (t) => { 318 | t.plan(1) 319 | const stringify = build({ 320 | title: 'additionalProperties', 321 | type: 'object', 322 | additionalProperties: true, 323 | properties: { 324 | str: { 325 | type: 'string' 326 | } 327 | } 328 | }) 329 | 330 | const obj = { str: 'x', test: 'test', meth: () => 'x', sym: Symbol('x') } 331 | t.assert.equal(stringify(obj), '{"str":"x","test":"test"}') 332 | }) 333 | -------------------------------------------------------------------------------- /test/any.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('object with nested random property', (t) => { 7 | t.plan(4) 8 | 9 | const schema = { 10 | title: 'empty schema to allow any object', 11 | type: 'object', 12 | properties: { 13 | id: { type: 'number' }, 14 | name: {} 15 | } 16 | } 17 | const stringify = build(schema) 18 | 19 | t.assert.equal(stringify({ 20 | id: 1, name: 'string' 21 | }), '{"id":1,"name":"string"}') 22 | 23 | t.assert.equal(stringify({ 24 | id: 1, name: { first: 'name', last: 'last' } 25 | }), '{"id":1,"name":{"first":"name","last":"last"}}') 26 | 27 | t.assert.equal(stringify({ 28 | id: 1, name: null 29 | }), '{"id":1,"name":null}') 30 | 31 | t.assert.equal(stringify({ 32 | id: 1, name: ['first', 'last'] 33 | }), '{"id":1,"name":["first","last"]}') 34 | }) 35 | 36 | // reference: https://github.com/fastify/fast-json-stringify/issues/259 37 | test('object with empty schema with $id: undefined set', (t) => { 38 | t.plan(1) 39 | 40 | const schema = { 41 | title: 'empty schema to allow any object with $id: undefined set', 42 | type: 'object', 43 | properties: { 44 | name: { $id: undefined } 45 | } 46 | } 47 | const stringify = build(schema) 48 | t.assert.equal(stringify({ 49 | name: 'string' 50 | }), '{"name":"string"}') 51 | }) 52 | 53 | test('array with random items', (t) => { 54 | t.plan(1) 55 | 56 | const schema = { 57 | title: 'empty schema to allow any object', 58 | type: 'array', 59 | items: {} 60 | } 61 | const stringify = build(schema) 62 | 63 | const value = stringify([1, 'string', null]) 64 | t.assert.equal(value, '[1,"string",null]') 65 | }) 66 | 67 | test('empty schema', (t) => { 68 | t.plan(7) 69 | 70 | const schema = { } 71 | 72 | const stringify = build(schema) 73 | 74 | t.assert.equal(stringify(null), 'null') 75 | t.assert.equal(stringify(1), '1') 76 | t.assert.equal(stringify(true), 'true') 77 | t.assert.equal(stringify('hello'), '"hello"') 78 | t.assert.equal(stringify({}), '{}') 79 | t.assert.equal(stringify({ x: 10 }), '{"x":10}') 80 | t.assert.equal(stringify([true, 1, 'hello']), '[true,1,"hello"]') 81 | }) 82 | 83 | test('empty schema on nested object', (t) => { 84 | t.plan(7) 85 | 86 | const schema = { 87 | type: 'object', 88 | properties: { 89 | x: {} 90 | } 91 | } 92 | 93 | const stringify = build(schema) 94 | 95 | t.assert.equal(stringify({ x: null }), '{"x":null}') 96 | t.assert.equal(stringify({ x: 1 }), '{"x":1}') 97 | t.assert.equal(stringify({ x: true }), '{"x":true}') 98 | t.assert.equal(stringify({ x: 'hello' }), '{"x":"hello"}') 99 | t.assert.equal(stringify({ x: {} }), '{"x":{}}') 100 | t.assert.equal(stringify({ x: { x: 10 } }), '{"x":{"x":10}}') 101 | t.assert.equal(stringify({ x: [true, 1, 'hello'] }), '{"x":[true,1,"hello"]}') 102 | }) 103 | 104 | test('empty schema on array', (t) => { 105 | t.plan(1) 106 | 107 | const schema = { 108 | type: 'array', 109 | items: {} 110 | } 111 | 112 | const stringify = build(schema) 113 | 114 | t.assert.equal(stringify([1, true, 'hello', [], { x: 1 }]), '[1,true,"hello",[],{"x":1}]') 115 | }) 116 | 117 | test('empty schema on anyOf', (t) => { 118 | t.plan(4) 119 | 120 | // any on Foo codepath. 121 | const schema = { 122 | anyOf: [ 123 | { 124 | type: 'object', 125 | properties: { 126 | kind: { 127 | type: 'string', 128 | enum: ['Foo'] 129 | }, 130 | value: {} 131 | } 132 | }, 133 | { 134 | type: 'object', 135 | properties: { 136 | kind: { 137 | type: 'string', 138 | enum: ['Bar'] 139 | }, 140 | value: { 141 | type: 'number' 142 | } 143 | } 144 | } 145 | ] 146 | } 147 | 148 | const stringify = build(schema) 149 | 150 | t.assert.equal(stringify({ kind: 'Bar', value: 1 }), '{"kind":"Bar","value":1}') 151 | t.assert.equal(stringify({ kind: 'Foo', value: 1 }), '{"kind":"Foo","value":1}') 152 | t.assert.equal(stringify({ kind: 'Foo', value: true }), '{"kind":"Foo","value":true}') 153 | t.assert.equal(stringify({ kind: 'Foo', value: 'hello' }), '{"kind":"Foo","value":"hello"}') 154 | }) 155 | 156 | test('should throw a TypeError with the path to the key of the invalid value /1', (t) => { 157 | t.plan(1) 158 | 159 | // any on Foo codepath. 160 | const schema = { 161 | anyOf: [ 162 | { 163 | type: 'object', 164 | properties: { 165 | kind: { 166 | type: 'string', 167 | enum: ['Foo'] 168 | }, 169 | value: {} 170 | } 171 | }, 172 | { 173 | type: 'object', 174 | properties: { 175 | kind: { 176 | type: 'string', 177 | enum: ['Bar'] 178 | }, 179 | value: { 180 | type: 'number' 181 | } 182 | } 183 | } 184 | ] 185 | } 186 | 187 | const stringify = build(schema) 188 | 189 | t.assert.throws(() => stringify({ kind: 'Baz', value: 1 }), new TypeError('The value of \'#\' does not match schema definition.')) 190 | }) 191 | 192 | test('should throw a TypeError with the path to the key of the invalid value /2', (t) => { 193 | t.plan(1) 194 | 195 | // any on Foo codepath. 196 | const schema = { 197 | type: 'object', 198 | properties: { 199 | data: { 200 | anyOf: [ 201 | { 202 | type: 'object', 203 | properties: { 204 | kind: { 205 | type: 'string', 206 | enum: ['Foo'] 207 | }, 208 | value: {} 209 | } 210 | }, 211 | { 212 | type: 'object', 213 | properties: { 214 | kind: { 215 | type: 'string', 216 | enum: ['Bar'] 217 | }, 218 | value: { 219 | type: 'number' 220 | } 221 | } 222 | } 223 | ] 224 | } 225 | } 226 | } 227 | 228 | const stringify = build(schema) 229 | 230 | t.assert.throws(() => stringify({ data: { kind: 'Baz', value: 1 } }), new TypeError('The value of \'#/properties/data\' does not match schema definition.')) 231 | }) 232 | -------------------------------------------------------------------------------- /test/asNumber.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | 5 | test('asNumber should convert BigInt', (t) => { 6 | t.plan(1) 7 | const Serializer = require('../lib/serializer') 8 | const serializer = new Serializer() 9 | 10 | const number = serializer.asNumber(11753021440n) 11 | 12 | t.assert.equal(number, '11753021440') 13 | }) 14 | -------------------------------------------------------------------------------- /test/basic.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const validator = require('is-my-json-valid') 5 | const build = require('..') 6 | 7 | function buildTest (schema, toStringify) { 8 | test(`render a ${schema.title} as JSON`, (t) => { 9 | t.plan(3) 10 | 11 | const validate = validator(schema) 12 | const stringify = build(schema) 13 | const output = stringify(toStringify) 14 | 15 | t.assert.deepStrictEqual(JSON.parse(output), toStringify) 16 | t.assert.equal(output, JSON.stringify(toStringify)) 17 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 18 | }) 19 | } 20 | 21 | buildTest({ 22 | title: 'string', 23 | type: 'string', 24 | format: 'unsafe' 25 | }, 'hello world') 26 | 27 | buildTest({ 28 | title: 'basic', 29 | type: 'object', 30 | properties: { 31 | firstName: { 32 | type: 'string' 33 | }, 34 | lastName: { 35 | type: 'string' 36 | }, 37 | age: { 38 | description: 'Age in years', 39 | type: 'integer', 40 | minimum: 0 41 | }, 42 | magic: { 43 | type: 'number' 44 | } 45 | }, 46 | required: ['firstName', 'lastName'] 47 | }, { 48 | firstName: 'Matteo', 49 | lastName: 'Collina', 50 | age: 32, 51 | magic: 42.42 52 | }) 53 | 54 | buildTest({ 55 | title: 'string', 56 | type: 'string' 57 | }, 'hello world') 58 | 59 | buildTest({ 60 | title: 'string', 61 | type: 'string' 62 | }, 'hello\nworld') 63 | 64 | buildTest({ 65 | title: 'string with quotes', 66 | type: 'string' 67 | }, 'hello """" world') 68 | 69 | buildTest({ 70 | title: 'boolean true', 71 | type: 'boolean' 72 | }, true) 73 | 74 | buildTest({ 75 | title: 'boolean false', 76 | type: 'boolean' 77 | }, false) 78 | 79 | buildTest({ 80 | title: 'an integer', 81 | type: 'integer' 82 | }, 42) 83 | 84 | buildTest({ 85 | title: 'a number', 86 | type: 'number' 87 | }, 42.42) 88 | 89 | buildTest({ 90 | title: 'deep', 91 | type: 'object', 92 | properties: { 93 | firstName: { 94 | type: 'string' 95 | }, 96 | lastName: { 97 | type: 'string' 98 | }, 99 | more: { 100 | description: 'more properties', 101 | type: 'object', 102 | properties: { 103 | something: { 104 | type: 'string' 105 | } 106 | } 107 | } 108 | } 109 | }, { 110 | firstName: 'Matteo', 111 | lastName: 'Collina', 112 | more: { 113 | something: 'else' 114 | } 115 | }) 116 | 117 | buildTest({ 118 | title: 'null', 119 | type: 'null' 120 | }, null) 121 | 122 | buildTest({ 123 | title: 'deep object with weird keys', 124 | type: 'object', 125 | properties: { 126 | '@version': { 127 | type: 'integer' 128 | } 129 | } 130 | }, { 131 | '@version': 1 132 | }) 133 | 134 | buildTest({ 135 | title: 'deep object with weird keys of type object', 136 | type: 'object', 137 | properties: { 138 | '@data': { 139 | type: 'object', 140 | properties: { 141 | id: { 142 | type: 'string' 143 | } 144 | } 145 | } 146 | } 147 | }, { 148 | '@data': { 149 | id: 'string' 150 | } 151 | }) 152 | 153 | buildTest({ 154 | title: 'deep object with spaces in key', 155 | type: 'object', 156 | properties: { 157 | 'spaces in key': { 158 | type: 'object', 159 | properties: { 160 | something: { 161 | type: 'integer' 162 | } 163 | } 164 | } 165 | } 166 | }, { 167 | 'spaces in key': { 168 | something: 1 169 | } 170 | }) 171 | 172 | buildTest({ 173 | title: 'with null', 174 | type: 'object', 175 | properties: { 176 | firstName: { 177 | type: 'null' 178 | } 179 | } 180 | }, { 181 | firstName: null 182 | }) 183 | 184 | buildTest({ 185 | title: 'array with objects', 186 | type: 'array', 187 | items: { 188 | type: 'object', 189 | properties: { 190 | name: { 191 | type: 'string' 192 | } 193 | } 194 | } 195 | }, [{ 196 | name: 'Matteo' 197 | }, { 198 | name: 'Dave' 199 | }]) 200 | 201 | buildTest({ 202 | title: 'array with strings', 203 | type: 'array', 204 | items: { 205 | type: 'string' 206 | } 207 | }, [ 208 | 'Matteo', 209 | 'Dave' 210 | ]) 211 | 212 | buildTest({ 213 | title: 'array with numbers', 214 | type: 'array', 215 | items: { 216 | type: 'number' 217 | } 218 | }, [ 219 | 42.42, 220 | 24 221 | ]) 222 | 223 | buildTest({ 224 | title: 'array with integers', 225 | type: 'array', 226 | items: { 227 | type: 'number' 228 | } 229 | }, [ 230 | 42, 231 | 24 232 | ]) 233 | 234 | buildTest({ 235 | title: 'nested array with objects', 236 | type: 'object', 237 | properties: { 238 | data: { 239 | type: 'array', 240 | items: { 241 | type: 'object', 242 | properties: { 243 | name: { 244 | type: 'string' 245 | } 246 | } 247 | } 248 | } 249 | } 250 | }, { 251 | data: [{ 252 | name: 'Matteo' 253 | }, { 254 | name: 'Dave' 255 | }] 256 | }) 257 | 258 | buildTest({ 259 | title: 'object with boolean', 260 | type: 'object', 261 | properties: { 262 | readonly: { 263 | type: 'boolean' 264 | } 265 | } 266 | }, { 267 | readonly: true 268 | }) 269 | 270 | test('throw an error or coerce numbers and integers that are not numbers', (t) => { 271 | const stringify = build({ 272 | title: 'basic', 273 | type: 'object', 274 | properties: { 275 | age: { 276 | type: 'number' 277 | }, 278 | distance: { 279 | type: 'integer' 280 | } 281 | } 282 | }) 283 | 284 | t.assert.throws(() => { 285 | stringify({ age: 'hello ', distance: 'long' }) 286 | }, { message: 'The value "hello " cannot be converted to a number.' }) 287 | 288 | const result = stringify({ 289 | age: '42', 290 | distance: true 291 | }) 292 | 293 | t.assert.deepStrictEqual(JSON.parse(result), { age: 42, distance: 1 }) 294 | }) 295 | 296 | test('Should throw on invalid schema', t => { 297 | t.plan(1) 298 | t.assert.throws(() => { 299 | build({ 300 | type: 'Dinosaur', 301 | properties: { 302 | claws: { type: 'sharp' } 303 | } 304 | }) 305 | }, { message: 'schema is invalid: data/properties/claws/type must be equal to one of the allowed values' }) 306 | }) 307 | 308 | test('additionalProperties - throw on unknown type', (t) => { 309 | t.plan(1) 310 | 311 | t.assert.throws(() => { 312 | build({ 313 | title: 'check array coerce', 314 | type: 'object', 315 | properties: {}, 316 | additionalProperties: { 317 | type: 'strangetype' 318 | } 319 | }) 320 | t.fail('should be an invalid schema') 321 | }, { message: 'schema is invalid: data/additionalProperties/type must be equal to one of the allowed values' }) 322 | }) 323 | 324 | test('patternProperties - throw on unknown type', (t) => { 325 | t.plan(1) 326 | 327 | t.assert.throws(() => { 328 | build({ 329 | title: 'check array coerce', 330 | type: 'object', 331 | properties: {}, 332 | patternProperties: { 333 | foo: { 334 | type: 'strangetype' 335 | } 336 | } 337 | }) 338 | }, { message: 'schema is invalid: data/patternProperties/foo/type must be equal to one of the allowed values' }) 339 | }) 340 | 341 | test('render a double quote as JSON /1', (t) => { 342 | t.plan(2) 343 | 344 | const schema = { 345 | type: 'string' 346 | } 347 | const toStringify = '" double quote' 348 | 349 | const validate = validator(schema) 350 | const stringify = build(schema) 351 | const output = stringify(toStringify) 352 | 353 | t.assert.equal(output, JSON.stringify(toStringify)) 354 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 355 | }) 356 | 357 | test('render a double quote as JSON /2', (t) => { 358 | t.plan(2) 359 | 360 | const schema = { 361 | type: 'string' 362 | } 363 | const toStringify = 'double quote " 2' 364 | 365 | const validate = validator(schema) 366 | const stringify = build(schema) 367 | const output = stringify(toStringify) 368 | 369 | t.assert.equal(output, JSON.stringify(toStringify)) 370 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 371 | }) 372 | 373 | test('render a long string', (t) => { 374 | t.plan(2) 375 | 376 | const schema = { 377 | type: 'string' 378 | } 379 | const toStringify = 'the Ultimate Question of Life, the Universe, and Everything.' 380 | 381 | const validate = validator(schema) 382 | const stringify = build(schema) 383 | const output = stringify(toStringify) 384 | 385 | t.assert.equal(output, JSON.stringify(toStringify)) 386 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 387 | }) 388 | 389 | test('returns JSON.stringify if schema type is boolean', t => { 390 | t.plan(1) 391 | 392 | const schema = { 393 | type: 'array', 394 | items: true 395 | } 396 | 397 | const array = [1, true, 'test'] 398 | const stringify = build(schema) 399 | t.assert.equal(stringify(array), JSON.stringify(array)) 400 | }) 401 | -------------------------------------------------------------------------------- /test/bigint.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | 5 | const build = require('..') 6 | 7 | test('render a bigint as JSON', (t) => { 8 | t.plan(1) 9 | 10 | const schema = { 11 | title: 'bigint', 12 | type: 'integer' 13 | } 14 | 15 | const stringify = build(schema) 16 | const output = stringify(1615n) 17 | 18 | t.assert.equal(output, '1615') 19 | }) 20 | 21 | test('render an object with a bigint as JSON', (t) => { 22 | t.plan(1) 23 | 24 | const schema = { 25 | title: 'object with bigint', 26 | type: 'object', 27 | properties: { 28 | id: { 29 | type: 'integer' 30 | } 31 | } 32 | } 33 | 34 | const stringify = build(schema) 35 | const output = stringify({ 36 | id: 1615n 37 | }) 38 | 39 | t.assert.equal(output, '{"id":1615}') 40 | }) 41 | 42 | test('render an array with a bigint as JSON', (t) => { 43 | t.plan(1) 44 | 45 | const schema = { 46 | title: 'array with bigint', 47 | type: 'array', 48 | items: { 49 | type: 'integer' 50 | } 51 | } 52 | 53 | const stringify = build(schema) 54 | const output = stringify([1615n]) 55 | 56 | t.assert.equal(output, '[1615]') 57 | }) 58 | 59 | test('render an object with an additionalProperty of type bigint as JSON', (t) => { 60 | t.plan(1) 61 | 62 | const schema = { 63 | title: 'object with bigint', 64 | type: 'object', 65 | additionalProperties: { 66 | type: 'integer' 67 | } 68 | } 69 | 70 | const stringify = build(schema) 71 | const output = stringify({ 72 | num: 1615n 73 | }) 74 | 75 | t.assert.equal(output, '{"num":1615}') 76 | }) 77 | -------------------------------------------------------------------------------- /test/clean-cache.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('Should clean the cache', (t) => { 7 | t.plan(1) 8 | 9 | const schema = { 10 | $id: 'test', 11 | type: 'string' 12 | } 13 | 14 | t.assert.doesNotThrow(() => { 15 | build(schema) 16 | build(schema) 17 | }) 18 | }) 19 | 20 | test('Should clean the cache with external schemas', (t) => { 21 | t.plan(1) 22 | 23 | const schema = { 24 | $id: 'test', 25 | definitions: { 26 | def: { 27 | type: 'object', 28 | properties: { 29 | str: { 30 | type: 'string' 31 | } 32 | } 33 | } 34 | }, 35 | type: 'object', 36 | properties: { 37 | obj: { 38 | $ref: '#/definitions/def' 39 | } 40 | } 41 | } 42 | 43 | t.assert.doesNotThrow(() => { 44 | build(schema) 45 | build(schema) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /test/const.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const validator = require('is-my-json-valid') 5 | const build = require('..') 6 | 7 | test('schema with const string', (t) => { 8 | t.plan(2) 9 | 10 | const schema = { 11 | type: 'object', 12 | properties: { 13 | foo: { const: 'bar' } 14 | } 15 | } 16 | 17 | const validate = validator(schema) 18 | const stringify = build(schema) 19 | const output = stringify({ 20 | foo: 'bar' 21 | }) 22 | 23 | t.assert.equal(output, '{"foo":"bar"}') 24 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 25 | }) 26 | 27 | test('schema with const string and different input', (t) => { 28 | t.plan(2) 29 | 30 | const schema = { 31 | type: 'object', 32 | properties: { 33 | foo: { const: 'bar' } 34 | } 35 | } 36 | 37 | const validate = validator(schema) 38 | const stringify = build(schema) 39 | const output = stringify({ 40 | foo: 'baz' 41 | }) 42 | 43 | t.assert.equal(output, '{"foo":"bar"}') 44 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 45 | }) 46 | 47 | test('schema with const string and different type input', (t) => { 48 | t.plan(2) 49 | 50 | const schema = { 51 | type: 'object', 52 | properties: { 53 | foo: { const: 'bar' } 54 | } 55 | } 56 | 57 | const validate = validator(schema) 58 | const stringify = build(schema) 59 | const output = stringify({ 60 | foo: 1 61 | }) 62 | 63 | t.assert.equal(output, '{"foo":"bar"}') 64 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 65 | }) 66 | 67 | test('schema with const string and no input', (t) => { 68 | t.plan(2) 69 | 70 | const schema = { 71 | type: 'object', 72 | properties: { 73 | foo: { const: 'bar' } 74 | } 75 | } 76 | 77 | const validate = validator(schema) 78 | const stringify = build(schema) 79 | const output = stringify({}) 80 | 81 | t.assert.equal(output, '{}') 82 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 83 | }) 84 | 85 | test('schema with const string that contains \'', (t) => { 86 | t.plan(2) 87 | 88 | const schema = { 89 | type: 'object', 90 | properties: { 91 | foo: { const: "'bar'" } 92 | } 93 | } 94 | 95 | const validate = validator(schema) 96 | const stringify = build(schema) 97 | const output = stringify({ 98 | foo: "'bar'" 99 | }) 100 | 101 | t.assert.equal(output, '{"foo":"\'bar\'"}') 102 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 103 | }) 104 | 105 | test('schema with const number', (t) => { 106 | t.plan(2) 107 | 108 | const schema = { 109 | type: 'object', 110 | properties: { 111 | foo: { const: 1 } 112 | } 113 | } 114 | 115 | const validate = validator(schema) 116 | const stringify = build(schema) 117 | const output = stringify({ 118 | foo: 1 119 | }) 120 | 121 | t.assert.equal(output, '{"foo":1}') 122 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 123 | }) 124 | 125 | test('schema with const number and different input', (t) => { 126 | t.plan(2) 127 | 128 | const schema = { 129 | type: 'object', 130 | properties: { 131 | foo: { const: 1 } 132 | } 133 | } 134 | 135 | const validate = validator(schema) 136 | const stringify = build(schema) 137 | const output = stringify({ 138 | foo: 2 139 | }) 140 | 141 | t.assert.equal(output, '{"foo":1}') 142 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 143 | }) 144 | 145 | test('schema with const bool', (t) => { 146 | t.plan(2) 147 | 148 | const schema = { 149 | type: 'object', 150 | properties: { 151 | foo: { const: true } 152 | } 153 | } 154 | 155 | const validate = validator(schema) 156 | const stringify = build(schema) 157 | const output = stringify({ 158 | foo: true 159 | }) 160 | 161 | t.assert.equal(output, '{"foo":true}') 162 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 163 | }) 164 | 165 | test('schema with const number', (t) => { 166 | t.plan(2) 167 | 168 | const schema = { 169 | type: 'object', 170 | properties: { 171 | foo: { const: 1 } 172 | } 173 | } 174 | 175 | const validate = validator(schema) 176 | const stringify = build(schema) 177 | const output = stringify({ 178 | foo: 1 179 | }) 180 | 181 | t.assert.equal(output, '{"foo":1}') 182 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 183 | }) 184 | 185 | test('schema with const null', (t) => { 186 | t.plan(2) 187 | 188 | const schema = { 189 | type: 'object', 190 | properties: { 191 | foo: { const: null } 192 | } 193 | } 194 | 195 | const validate = validator(schema) 196 | const stringify = build(schema) 197 | const output = stringify({ 198 | foo: null 199 | }) 200 | 201 | t.assert.equal(output, '{"foo":null}') 202 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 203 | }) 204 | 205 | test('schema with const array', (t) => { 206 | t.plan(2) 207 | 208 | const schema = { 209 | type: 'object', 210 | properties: { 211 | foo: { const: [1, 2, 3] } 212 | } 213 | } 214 | 215 | const validate = validator(schema) 216 | const stringify = build(schema) 217 | const output = stringify({ 218 | foo: [1, 2, 3] 219 | }) 220 | 221 | t.assert.equal(output, '{"foo":[1,2,3]}') 222 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 223 | }) 224 | 225 | test('schema with const object', (t) => { 226 | t.plan(2) 227 | 228 | const schema = { 229 | type: 'object', 230 | properties: { 231 | foo: { const: { bar: 'baz' } } 232 | } 233 | } 234 | 235 | const validate = validator(schema) 236 | const stringify = build(schema) 237 | const output = stringify({ 238 | foo: { bar: 'baz' } 239 | }) 240 | 241 | t.assert.equal(output, '{"foo":{"bar":"baz"}}') 242 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 243 | }) 244 | 245 | test('schema with const and null as type', (t) => { 246 | t.plan(4) 247 | 248 | const schema = { 249 | type: 'object', 250 | properties: { 251 | foo: { type: ['string', 'null'], const: 'baz' } 252 | } 253 | } 254 | 255 | const validate = validator(schema) 256 | const stringify = build(schema) 257 | const output = stringify({ 258 | foo: null 259 | }) 260 | 261 | t.assert.equal(output, '{"foo":null}') 262 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 263 | 264 | const output2 = stringify({ foo: 'baz' }) 265 | t.assert.equal(output2, '{"foo":"baz"}') 266 | t.assert.ok(validate(JSON.parse(output2)), 'valid schema') 267 | }) 268 | 269 | test('schema with const as nullable', (t) => { 270 | t.plan(4) 271 | 272 | const schema = { 273 | type: 'object', 274 | properties: { 275 | foo: { nullable: true, const: 'baz' } 276 | } 277 | } 278 | 279 | const validate = validator(schema) 280 | const stringify = build(schema) 281 | const output = stringify({ 282 | foo: null 283 | }) 284 | 285 | t.assert.equal(output, '{"foo":null}') 286 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 287 | 288 | const output2 = stringify({ 289 | foo: 'baz' 290 | }) 291 | t.assert.equal(output2, '{"foo":"baz"}') 292 | t.assert.ok(validate(JSON.parse(output2)), 'valid schema') 293 | }) 294 | 295 | test('schema with const and invalid object', (t) => { 296 | t.plan(2) 297 | 298 | const schema = { 299 | type: 'object', 300 | properties: { 301 | foo: { const: { foo: 'bar' } } 302 | }, 303 | required: ['foo'] 304 | } 305 | 306 | const validate = validator(schema) 307 | const stringify = build(schema) 308 | const result = stringify({ 309 | foo: { foo: 'baz' } 310 | }) 311 | 312 | t.assert.equal(result, '{"foo":{"foo":"bar"}}') 313 | t.assert.ok(validate(JSON.parse(result)), 'valid schema') 314 | }) 315 | -------------------------------------------------------------------------------- /test/debug-mode.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const fjs = require('..') 5 | 6 | const Ajv = require('ajv').default 7 | const Validator = require('../lib/validator') 8 | const Serializer = require('../lib/serializer') 9 | 10 | function build (opts) { 11 | return fjs({ 12 | title: 'default string', 13 | type: 'object', 14 | properties: { 15 | firstName: { 16 | type: 'string' 17 | } 18 | }, 19 | required: ['firstName'] 20 | }, opts) 21 | } 22 | 23 | test('activate debug mode', t => { 24 | t.plan(5) 25 | const debugMode = build({ debugMode: true }) 26 | 27 | t.assert.ok(typeof debugMode === 'object') 28 | t.assert.ok(debugMode.ajv instanceof Ajv) 29 | t.assert.ok(debugMode.validator instanceof Validator) 30 | t.assert.ok(debugMode.serializer instanceof Serializer) 31 | t.assert.ok(typeof debugMode.code === 'string') 32 | }) 33 | 34 | test('activate debug mode truthy', t => { 35 | t.plan(5) 36 | 37 | const debugMode = build({ debugMode: 'yes' }) 38 | 39 | t.assert.ok(typeof debugMode === 'object') 40 | t.assert.ok(typeof debugMode.code === 'string') 41 | t.assert.ok(debugMode.ajv instanceof Ajv) 42 | t.assert.ok(debugMode.validator instanceof Validator) 43 | t.assert.ok(debugMode.serializer instanceof Serializer) 44 | }) 45 | 46 | test('to string auto-consistent', t => { 47 | t.plan(6) 48 | const debugMode = build({ debugMode: 1 }) 49 | 50 | t.assert.ok(typeof debugMode === 'object') 51 | t.assert.ok(typeof debugMode.code === 'string') 52 | t.assert.ok(debugMode.ajv instanceof Ajv) 53 | t.assert.ok(debugMode.serializer instanceof Serializer) 54 | t.assert.ok(debugMode.validator instanceof Validator) 55 | 56 | const compiled = fjs.restore(debugMode) 57 | const tobe = JSON.stringify({ firstName: 'Foo' }) 58 | t.assert.equal(compiled({ firstName: 'Foo', surname: 'bar' }), tobe, 'surname evicted') 59 | }) 60 | 61 | test('to string auto-consistent with ajv', t => { 62 | t.plan(6) 63 | 64 | const debugMode = fjs({ 65 | title: 'object with multiple types field', 66 | type: 'object', 67 | properties: { 68 | str: { 69 | anyOf: [{ 70 | type: 'string' 71 | }, { 72 | type: 'boolean' 73 | }] 74 | } 75 | } 76 | }, { debugMode: 1 }) 77 | 78 | t.assert.ok(typeof debugMode === 'object') 79 | t.assert.ok(typeof debugMode.code === 'string') 80 | t.assert.ok(debugMode.ajv instanceof Ajv) 81 | t.assert.ok(debugMode.validator instanceof Validator) 82 | t.assert.ok(debugMode.serializer instanceof Serializer) 83 | 84 | const compiled = fjs.restore(debugMode) 85 | const tobe = JSON.stringify({ str: 'Foo' }) 86 | t.assert.equal(compiled({ str: 'Foo', void: 'me' }), tobe) 87 | }) 88 | 89 | test('to string auto-consistent with ajv-formats', t => { 90 | t.plan(3) 91 | 92 | const debugMode = fjs({ 93 | title: 'object with multiple types field and format keyword', 94 | type: 'object', 95 | properties: { 96 | str: { 97 | anyOf: [{ 98 | type: 'string', 99 | format: 'email' 100 | }, { 101 | type: 'boolean' 102 | }] 103 | } 104 | } 105 | }, { debugMode: 1 }) 106 | 107 | t.assert.ok(typeof debugMode === 'object') 108 | 109 | const compiled = fjs.restore(debugMode) 110 | const tobe = JSON.stringify({ str: 'foo@bar.com' }) 111 | t.assert.equal(compiled({ str: 'foo@bar.com' }), tobe) 112 | t.assert.throws(() => compiled({ str: 'foo' })) 113 | }) 114 | 115 | test('debug should restore the same serializer instance', t => { 116 | t.plan(1) 117 | 118 | const debugMode = fjs({ type: 'integer' }, { debugMode: 1, rounding: 'ceil' }) 119 | const compiled = fjs.restore(debugMode) 120 | t.assert.equal(compiled(3.95), 4) 121 | }) 122 | -------------------------------------------------------------------------------- /test/defaults.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | function buildTest (schema, toStringify, expected) { 7 | test(`render a ${schema.title} with default as JSON`, (t) => { 8 | t.plan(1) 9 | 10 | const stringify = build(schema) 11 | 12 | const output = stringify(toStringify) 13 | 14 | t.assert.equal(output, JSON.stringify(expected)) 15 | }) 16 | } 17 | 18 | buildTest({ 19 | title: 'default string', 20 | type: 'object', 21 | properties: { 22 | firstName: { 23 | type: 'string' 24 | }, 25 | lastName: { 26 | type: 'string', 27 | default: 'Collina' 28 | }, 29 | age: { 30 | description: 'Age in years', 31 | type: 'integer', 32 | minimum: 0 33 | }, 34 | magic: { 35 | type: 'number' 36 | } 37 | }, 38 | required: ['firstName', 'lastName'] 39 | }, { 40 | firstName: 'Matteo', 41 | magic: 42, 42 | age: 32 43 | }, { 44 | firstName: 'Matteo', 45 | lastName: 'Collina', 46 | age: 32, 47 | magic: 42 48 | }) 49 | 50 | buildTest({ 51 | title: 'default string with value', 52 | type: 'object', 53 | properties: { 54 | firstName: { 55 | type: 'string' 56 | }, 57 | lastName: { 58 | type: 'string', 59 | default: 'Collina' 60 | }, 61 | age: { 62 | description: 'Age in years', 63 | type: 'integer', 64 | minimum: 0 65 | }, 66 | magic: { 67 | type: 'number' 68 | } 69 | }, 70 | required: ['firstName', 'lastName'] 71 | }, { 72 | firstName: 'Matteo', 73 | lastName: 'collina', 74 | magic: 42, 75 | age: 32 76 | }, { 77 | firstName: 'Matteo', 78 | lastName: 'collina', 79 | age: 32, 80 | magic: 42 81 | }) 82 | 83 | buildTest({ 84 | title: 'default number', 85 | type: 'object', 86 | properties: { 87 | firstName: { 88 | type: 'string' 89 | }, 90 | lastName: { 91 | type: 'string' 92 | }, 93 | age: { 94 | description: 'Age in years', 95 | type: 'integer', 96 | minimum: 0 97 | }, 98 | magic: { 99 | type: 'number', 100 | default: 42 101 | } 102 | }, 103 | required: ['firstName', 'lastName'] 104 | }, { 105 | firstName: 'Matteo', 106 | lastName: 'Collina', 107 | age: 32 108 | }, { 109 | firstName: 'Matteo', 110 | lastName: 'Collina', 111 | age: 32, 112 | magic: 42 113 | }) 114 | 115 | buildTest({ 116 | title: 'default number with value', 117 | type: 'object', 118 | properties: { 119 | firstName: { 120 | type: 'string' 121 | }, 122 | lastName: { 123 | type: 'string' 124 | }, 125 | age: { 126 | description: 'Age in years', 127 | type: 'integer', 128 | minimum: 0 129 | }, 130 | magic: { 131 | type: 'number', 132 | default: 42 133 | } 134 | }, 135 | required: ['firstName', 'lastName'] 136 | }, { 137 | firstName: 'Matteo', 138 | lastName: 'Collina', 139 | age: 32, 140 | magic: 66 141 | }, { 142 | firstName: 'Matteo', 143 | lastName: 'Collina', 144 | age: 32, 145 | magic: 66 146 | }) 147 | 148 | buildTest({ 149 | title: 'default object', 150 | type: 'object', 151 | properties: { 152 | firstName: { 153 | type: 'string' 154 | }, 155 | lastName: { 156 | type: 'string' 157 | }, 158 | age: { 159 | description: 'Age in years', 160 | type: 'integer', 161 | minimum: 0 162 | }, 163 | otherProps: { 164 | type: 'object', 165 | default: { foo: 'bar' } 166 | } 167 | }, 168 | required: ['firstName', 'lastName'] 169 | }, { 170 | firstName: 'Matteo', 171 | lastName: 'Collina', 172 | age: 32 173 | }, { 174 | firstName: 'Matteo', 175 | lastName: 'Collina', 176 | age: 32, 177 | otherProps: { foo: 'bar' } 178 | }) 179 | 180 | buildTest({ 181 | title: 'default object with value', 182 | type: 'object', 183 | properties: { 184 | firstName: { 185 | type: 'string' 186 | }, 187 | lastName: { 188 | type: 'string' 189 | }, 190 | age: { 191 | description: 'Age in years', 192 | type: 'integer', 193 | minimum: 0 194 | }, 195 | otherProps: { 196 | type: 'object', 197 | additionalProperties: true, 198 | default: { foo: 'bar' } 199 | } 200 | }, 201 | required: ['firstName', 'lastName'] 202 | }, { 203 | firstName: 'Matteo', 204 | lastName: 'Collina', 205 | age: 32, 206 | otherProps: { hello: 'world' } 207 | }, { 208 | firstName: 'Matteo', 209 | lastName: 'Collina', 210 | age: 32, 211 | otherProps: { hello: 'world' } 212 | }) 213 | 214 | buildTest({ 215 | title: 'default array', 216 | type: 'object', 217 | properties: { 218 | firstName: { 219 | type: 'string' 220 | }, 221 | lastName: { 222 | type: 'string' 223 | }, 224 | age: { 225 | description: 'Age in years', 226 | type: 'integer', 227 | minimum: 0 228 | }, 229 | otherProps: { 230 | type: 'array', 231 | items: { type: 'string' }, 232 | default: ['FOO'] 233 | } 234 | }, 235 | required: ['firstName', 'lastName'] 236 | }, { 237 | firstName: 'Matteo', 238 | lastName: 'Collina', 239 | age: 32 240 | }, { 241 | firstName: 'Matteo', 242 | lastName: 'Collina', 243 | age: 32, 244 | otherProps: ['FOO'] 245 | }) 246 | 247 | buildTest({ 248 | title: 'default array with value', 249 | type: 'object', 250 | properties: { 251 | firstName: { 252 | type: 'string' 253 | }, 254 | lastName: { 255 | type: 'string' 256 | }, 257 | age: { 258 | description: 'Age in years', 259 | type: 'integer', 260 | minimum: 0 261 | }, 262 | otherProps: { 263 | type: 'array', 264 | items: { type: 'string' }, 265 | default: ['FOO'] 266 | } 267 | }, 268 | required: ['firstName', 'lastName'] 269 | }, { 270 | firstName: 'Matteo', 271 | lastName: 'Collina', 272 | age: 32, 273 | otherProps: ['BAR'] 274 | }, { 275 | firstName: 'Matteo', 276 | lastName: 'Collina', 277 | age: 32, 278 | otherProps: ['BAR'] 279 | }) 280 | 281 | buildTest({ 282 | title: 'default deeper value', 283 | type: 'object', 284 | properties: { 285 | level1: { 286 | type: 'object', 287 | properties: { 288 | level2: { 289 | type: 'object', 290 | properties: { 291 | level3: { 292 | type: 'object', 293 | properties: { 294 | level4: { 295 | type: 'object', 296 | default: { foo: 'bar' } 297 | } 298 | } 299 | } 300 | } 301 | } 302 | } 303 | } 304 | } 305 | }, { 306 | level1: { level2: { level3: { } } } 307 | }, { 308 | level1: { level2: { level3: { level4: { foo: 'bar' } } } } 309 | }) 310 | 311 | buildTest({ 312 | title: 'default deeper value with value', 313 | type: 'object', 314 | properties: { 315 | level1: { 316 | type: 'object', 317 | properties: { 318 | level2: { 319 | type: 'object', 320 | properties: { 321 | level3: { 322 | type: 'object', 323 | properties: { 324 | level4: { 325 | type: 'object', 326 | default: { foo: 'bar' } 327 | } 328 | } 329 | } 330 | } 331 | } 332 | } 333 | } 334 | } 335 | }, { 336 | level1: { level2: { level3: { level4: { } } } } 337 | }, { 338 | level1: { level2: { level3: { level4: { } } } } 339 | }) 340 | 341 | buildTest({ 342 | type: 'object', 343 | properties: { 344 | name: { 345 | type: 'string', 346 | default: 'foo' 347 | }, 348 | dev: { 349 | type: 'boolean', 350 | default: false 351 | } 352 | }, 353 | required: [ 354 | 'name', 'dev' 355 | ] 356 | }, {}, { name: 'foo', dev: false }) 357 | 358 | buildTest({ 359 | type: 'object', 360 | properties: { 361 | name: { 362 | type: 'string', 363 | default: 'foo' 364 | }, 365 | dev: { 366 | type: 'boolean' 367 | }, 368 | job: { 369 | type: 'string', 370 | default: 'awesome' 371 | } 372 | }, 373 | required: [ 374 | 'name', 'dev' 375 | ] 376 | }, { dev: true }, { name: 'foo', dev: true, job: 'awesome' }) 377 | -------------------------------------------------------------------------------- /test/enum.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('use enum without type', (t) => { 7 | t.plan(1) 8 | const stringify = build({ 9 | title: 'Example Schema', 10 | type: 'object', 11 | properties: { 12 | order: { 13 | type: 'string', 14 | enum: ['asc', 'desc'] 15 | } 16 | } 17 | }) 18 | 19 | const obj = { order: 'asc' } 20 | t.assert.equal('{"order":"asc"}', stringify(obj)) 21 | }) 22 | 23 | test('use enum without type', (t) => { 24 | t.plan(1) 25 | const stringify = build({ 26 | title: 'Example Schema', 27 | type: 'object', 28 | properties: { 29 | order: { 30 | enum: ['asc', 'desc'] 31 | } 32 | } 33 | }) 34 | 35 | const obj = { order: 'asc' } 36 | t.assert.equal('{"order":"asc"}', stringify(obj)) 37 | }) 38 | -------------------------------------------------------------------------------- /test/fix-604.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const fjs = require('..') 5 | 6 | test('fix-604', t => { 7 | const schema = { 8 | type: 'object', 9 | properties: { 10 | fullName: { type: 'string' }, 11 | phone: { type: 'number' } 12 | } 13 | } 14 | 15 | const input = { 16 | fullName: 'Jone', 17 | phone: 'phone' 18 | } 19 | 20 | const render = fjs(schema) 21 | 22 | t.assert.throws(() => { 23 | render(input) 24 | }, { message: 'The value "phone" cannot be converted to a number.' }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastify/fast-json-stringify/babe7d75eaa25c26a89ba80a2d8ec17fead0ef7b/test/fixtures/.keep -------------------------------------------------------------------------------- /test/if-then-else.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | process.env.TZ = 'UTC' 7 | 8 | const schema = { 9 | type: 'object', 10 | properties: { 11 | }, 12 | if: { 13 | type: 'object', 14 | properties: { 15 | kind: { type: 'string', enum: ['foobar'] } 16 | } 17 | }, 18 | then: { 19 | type: 'object', 20 | properties: { 21 | kind: { type: 'string', enum: ['foobar'] }, 22 | foo: { type: 'string' }, 23 | bar: { type: 'number' }, 24 | list: { 25 | type: 'array', 26 | items: { 27 | type: 'object', 28 | properties: { 29 | name: { type: 'string' }, 30 | value: { type: 'string' } 31 | } 32 | } 33 | } 34 | } 35 | }, 36 | else: { 37 | type: 'object', 38 | properties: { 39 | kind: { type: 'string', enum: ['greeting'] }, 40 | hi: { type: 'string' }, 41 | hello: { type: 'number' }, 42 | list: { 43 | type: 'array', 44 | items: { 45 | type: 'object', 46 | properties: { 47 | name: { type: 'string' }, 48 | value: { type: 'string' } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | const nestedIfSchema = { 57 | type: 'object', 58 | properties: { }, 59 | if: { 60 | type: 'object', 61 | properties: { 62 | kind: { type: 'string', enum: ['foobar', 'greeting'] } 63 | } 64 | }, 65 | then: { 66 | if: { 67 | type: 'object', 68 | properties: { 69 | kind: { type: 'string', enum: ['foobar'] } 70 | } 71 | }, 72 | then: { 73 | type: 'object', 74 | properties: { 75 | kind: { type: 'string', enum: ['foobar'] }, 76 | foo: { type: 'string' }, 77 | bar: { type: 'number' }, 78 | list: { 79 | type: 'array', 80 | items: { 81 | type: 'object', 82 | properties: { 83 | name: { type: 'string' }, 84 | value: { type: 'string' } 85 | } 86 | } 87 | } 88 | } 89 | }, 90 | else: { 91 | type: 'object', 92 | properties: { 93 | kind: { type: 'string', enum: ['greeting'] }, 94 | hi: { type: 'string' }, 95 | hello: { type: 'number' } 96 | } 97 | } 98 | }, 99 | else: { 100 | type: 'object', 101 | properties: { 102 | kind: { type: 'string', enum: ['alphabet'] }, 103 | a: { type: 'string' }, 104 | b: { type: 'number' } 105 | } 106 | } 107 | } 108 | 109 | const nestedElseSchema = { 110 | type: 'object', 111 | properties: { }, 112 | if: { 113 | type: 'object', 114 | properties: { 115 | kind: { type: 'string', enum: ['foobar'] } 116 | } 117 | }, 118 | then: { 119 | type: 'object', 120 | properties: { 121 | kind: { type: 'string', enum: ['foobar'] }, 122 | foo: { type: 'string' }, 123 | bar: { type: 'number' }, 124 | list: { 125 | type: 'array', 126 | items: { 127 | type: 'object', 128 | properties: { 129 | name: { type: 'string' }, 130 | value: { type: 'string' } 131 | } 132 | } 133 | } 134 | } 135 | }, 136 | else: { 137 | if: { 138 | type: 'object', 139 | properties: { 140 | kind: { type: 'string', enum: ['greeting'] } 141 | } 142 | }, 143 | then: { 144 | type: 'object', 145 | properties: { 146 | kind: { type: 'string', enum: ['greeting'] }, 147 | hi: { type: 'string' }, 148 | hello: { type: 'number' } 149 | } 150 | }, 151 | else: { 152 | type: 'object', 153 | properties: { 154 | kind: { type: 'string', enum: ['alphabet'] }, 155 | a: { type: 'string' }, 156 | b: { type: 'number' } 157 | } 158 | } 159 | } 160 | } 161 | 162 | const nestedDeepElseSchema = { 163 | type: 'object', 164 | additionalProperties: schema 165 | } 166 | 167 | const noElseSchema = { 168 | type: 'object', 169 | properties: { 170 | }, 171 | if: { 172 | type: 'object', 173 | properties: { 174 | kind: { type: 'string', enum: ['foobar'] } 175 | } 176 | }, 177 | then: { 178 | type: 'object', 179 | properties: { 180 | kind: { type: 'string', enum: ['foobar'] }, 181 | foo: { type: 'string' }, 182 | bar: { type: 'number' }, 183 | list: { 184 | type: 'array', 185 | items: { 186 | type: 'object', 187 | properties: { 188 | name: { type: 'string' }, 189 | value: { type: 'string' } 190 | } 191 | } 192 | } 193 | } 194 | } 195 | } 196 | const fooBarInput = { 197 | kind: 'foobar', 198 | foo: 'FOO', 199 | list: [{ 200 | name: 'name', 201 | value: 'foo' 202 | }], 203 | bar: 42, 204 | hi: 'HI', 205 | hello: 45, 206 | a: 'A', 207 | b: 35 208 | } 209 | const greetingInput = { 210 | kind: 'greeting', 211 | foo: 'FOO', 212 | bar: 42, 213 | hi: 'HI', 214 | hello: 45, 215 | a: 'A', 216 | b: 35 217 | } 218 | const alphabetInput = { 219 | kind: 'alphabet', 220 | foo: 'FOO', 221 | bar: 42, 222 | hi: 'HI', 223 | hello: 45, 224 | a: 'A', 225 | b: 35 226 | } 227 | const deepFoobarInput = { 228 | foobar: fooBarInput 229 | } 230 | const foobarOutput = JSON.stringify({ 231 | kind: 'foobar', 232 | foo: 'FOO', 233 | bar: 42, 234 | list: [{ 235 | name: 'name', 236 | value: 'foo' 237 | }] 238 | }) 239 | const greetingOutput = JSON.stringify({ 240 | kind: 'greeting', 241 | hi: 'HI', 242 | hello: 45 243 | }) 244 | const alphabetOutput = JSON.stringify({ 245 | kind: 'alphabet', 246 | a: 'A', 247 | b: 35 248 | }) 249 | const deepFoobarOutput = JSON.stringify({ 250 | foobar: JSON.parse(foobarOutput) 251 | }) 252 | const noElseGreetingOutput = JSON.stringify({}) 253 | 254 | test('if-then-else', async t => { 255 | const tests = [ 256 | { 257 | name: 'foobar', 258 | schema, 259 | input: fooBarInput, 260 | expected: foobarOutput 261 | }, 262 | { 263 | name: 'greeting', 264 | schema, 265 | input: greetingInput, 266 | expected: greetingOutput 267 | }, 268 | { 269 | name: 'if nested - then then', 270 | schema: nestedIfSchema, 271 | input: fooBarInput, 272 | expected: foobarOutput 273 | }, 274 | { 275 | name: 'if nested - then else', 276 | schema: nestedIfSchema, 277 | input: greetingInput, 278 | expected: greetingOutput 279 | }, 280 | { 281 | name: 'if nested - else', 282 | schema: nestedIfSchema, 283 | input: alphabetInput, 284 | expected: alphabetOutput 285 | }, 286 | { 287 | name: 'else nested - then', 288 | schema: nestedElseSchema, 289 | input: fooBarInput, 290 | expected: foobarOutput 291 | }, 292 | { 293 | name: 'else nested - else then', 294 | schema: nestedElseSchema, 295 | input: greetingInput, 296 | expected: greetingOutput 297 | }, 298 | { 299 | name: 'else nested - else else', 300 | schema: nestedElseSchema, 301 | input: alphabetInput, 302 | expected: alphabetOutput 303 | }, 304 | { 305 | name: 'deep then - else', 306 | schema: nestedDeepElseSchema, 307 | input: deepFoobarInput, 308 | expected: deepFoobarOutput 309 | }, 310 | { 311 | name: 'no else', 312 | schema: noElseSchema, 313 | input: greetingInput, 314 | expected: noElseGreetingOutput 315 | } 316 | ] 317 | 318 | for (const { name, schema, input, expected } of tests) { 319 | await t.test(name + ' - normal', async t => { 320 | t.plan(1) 321 | 322 | const stringify = build(JSON.parse(JSON.stringify(schema)), { ajv: { strictTypes: false } }) 323 | const serialized = stringify(input) 324 | t.assert.equal(serialized, expected) 325 | }) 326 | } 327 | }) 328 | 329 | test('nested if/then', t => { 330 | t.plan(2) 331 | 332 | const schema = { 333 | type: 'object', 334 | properties: { a: { type: 'string' } }, 335 | if: { 336 | type: 'object', 337 | properties: { foo: { type: 'string' } } 338 | }, 339 | then: { 340 | properties: { bar: { type: 'string' } }, 341 | if: { 342 | type: 'object', 343 | properties: { foo1: { type: 'string' } } 344 | }, 345 | then: { 346 | properties: { bar1: { type: 'string' } } 347 | } 348 | } 349 | } 350 | 351 | const stringify = build(schema) 352 | 353 | t.assert.equal( 354 | stringify({ a: 'A', foo: 'foo', bar: 'bar' }), 355 | JSON.stringify({ a: 'A', bar: 'bar' }) 356 | ) 357 | 358 | t.assert.equal( 359 | stringify({ a: 'A', foo: 'foo', bar: 'bar', foo1: 'foo1', bar1: 'bar1' }), 360 | JSON.stringify({ a: 'A', bar: 'bar', bar1: 'bar1' }) 361 | ) 362 | }) 363 | 364 | test('if/else with string format', (t) => { 365 | t.plan(2) 366 | 367 | const schema = { 368 | if: { type: 'string' }, 369 | then: { type: 'string', format: 'date' }, 370 | else: { const: 'Invalid' } 371 | } 372 | 373 | const stringify = build(schema) 374 | 375 | const date = new Date(1674263005800) 376 | 377 | t.assert.equal(stringify(date), '"2023-01-21"') 378 | t.assert.equal(stringify('Invalid'), '"Invalid"') 379 | }) 380 | 381 | test('if/else with const integers', (t) => { 382 | t.plan(2) 383 | 384 | const schema = { 385 | type: 'number', 386 | if: { type: 'number', minimum: 42 }, 387 | then: { const: 66 }, 388 | else: { const: 33 } 389 | } 390 | 391 | const stringify = build(schema) 392 | 393 | t.assert.equal(stringify(100.32), '66') 394 | t.assert.equal(stringify(10.12), '33') 395 | }) 396 | 397 | test('if/else with array', (t) => { 398 | t.plan(2) 399 | 400 | const schema = { 401 | type: 'array', 402 | if: { type: 'array', maxItems: 1 }, 403 | then: { items: { type: 'string' } }, 404 | else: { items: { type: 'number' } } 405 | } 406 | 407 | const stringify = build(schema) 408 | 409 | t.assert.equal(stringify(['1']), JSON.stringify(['1'])) 410 | t.assert.equal(stringify(['1', '2']), JSON.stringify([1, 2])) 411 | }) 412 | 413 | test('external recursive if/then/else', (t) => { 414 | t.plan(1) 415 | 416 | const externalSchema = { 417 | type: 'object', 418 | properties: { 419 | base: { type: 'string' }, 420 | self: { $ref: 'externalSchema#' } 421 | }, 422 | if: { 423 | type: 'object', 424 | properties: { 425 | foo: { type: 'string', const: '41' } 426 | } 427 | }, 428 | then: { 429 | type: 'object', 430 | properties: { 431 | bar: { type: 'string', const: '42' } 432 | } 433 | }, 434 | else: { 435 | type: 'object', 436 | properties: { 437 | baz: { type: 'string', const: '43' } 438 | } 439 | } 440 | } 441 | 442 | const schema = { 443 | type: 'object', 444 | properties: { 445 | a: { $ref: 'externalSchema#/properties/self' }, 446 | b: { $ref: 'externalSchema#/properties/self' } 447 | } 448 | } 449 | 450 | const data = { 451 | a: { 452 | base: 'a', 453 | foo: '41', 454 | bar: '42', 455 | baz: '43', 456 | ignore: 'ignored' 457 | }, 458 | b: { 459 | base: 'b', 460 | foo: 'not-41', 461 | bar: '42', 462 | baz: '43', 463 | ignore: 'ignored' 464 | } 465 | } 466 | const stringify = build(schema, { schema: { externalSchema } }) 467 | t.assert.equal(stringify(data), '{"a":{"base":"a","bar":"42"},"b":{"base":"b","baz":"43"}}') 468 | }) 469 | -------------------------------------------------------------------------------- /test/inferType.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const validator = require('is-my-json-valid') 5 | const build = require('..') 6 | 7 | function buildTest (schema, toStringify) { 8 | test(`render a ${schema.title} as JSON`, (t) => { 9 | t.plan(3) 10 | 11 | const validate = validator(schema) 12 | const stringify = build(schema) 13 | const output = stringify(toStringify) 14 | 15 | t.assert.deepStrictEqual(JSON.parse(output), toStringify) 16 | t.assert.equal(output, JSON.stringify(toStringify)) 17 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 18 | }) 19 | } 20 | 21 | buildTest({ 22 | title: 'infer type object by keyword', 23 | // 'type': 'object', 24 | properties: { 25 | name: { 26 | type: 'string' 27 | } 28 | } 29 | }, { 30 | name: 'foo' 31 | }) 32 | 33 | buildTest({ 34 | title: 'infer type of nested object by keyword', 35 | // 'type': 'object', 36 | properties: { 37 | more: { 38 | description: 'more properties', 39 | // 'type': 'object', 40 | properties: { 41 | something: { 42 | type: 'string' 43 | } 44 | } 45 | } 46 | } 47 | }, { 48 | more: { 49 | something: 'else' 50 | } 51 | }) 52 | 53 | buildTest({ 54 | title: 'infer type array by keyword', 55 | type: 'object', 56 | properties: { 57 | ids: { 58 | // 'type': 'array', 59 | items: { 60 | type: 'string' 61 | } 62 | } 63 | } 64 | }, { 65 | ids: ['test'] 66 | }) 67 | 68 | buildTest({ 69 | title: 'infer type string by keyword', 70 | type: 'object', 71 | properties: { 72 | name: { 73 | // 'type': 'string', 74 | maxLength: 3 75 | } 76 | } 77 | }, { 78 | name: 'foo' 79 | }) 80 | 81 | buildTest({ 82 | title: 'infer type number by keyword', 83 | type: 'object', 84 | properties: { 85 | age: { 86 | // 'type': 'number', 87 | maximum: 18 88 | } 89 | } 90 | }, { 91 | age: 18 92 | }) 93 | -------------------------------------------------------------------------------- /test/infinity.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('Finite numbers', t => { 7 | const values = [-5, 0, -0, 1.33, 99, 100.0, 8 | Math.E, Number.EPSILON, 9 | Number.MAX_SAFE_INTEGER, Number.MAX_VALUE, 10 | Number.MIN_SAFE_INTEGER, Number.MIN_VALUE] 11 | 12 | t.plan(values.length) 13 | 14 | const schema = { 15 | type: 'number' 16 | } 17 | 18 | const stringify = build(schema) 19 | 20 | values.forEach(v => t.assert.equal(stringify(v), JSON.stringify(v))) 21 | }) 22 | 23 | test('Infinite integers', t => { 24 | const values = [Infinity, -Infinity] 25 | 26 | t.plan(values.length) 27 | 28 | const schema = { 29 | type: 'integer' 30 | } 31 | 32 | const stringify = build(schema) 33 | 34 | values.forEach(v => { 35 | try { 36 | stringify(v) 37 | } catch (err) { 38 | t.assert.equal(err.message, `The value "${v}" cannot be converted to an integer.`) 39 | } 40 | }) 41 | }) 42 | 43 | test('Infinite numbers', t => { 44 | const values = [Infinity, -Infinity] 45 | 46 | t.plan(values.length) 47 | 48 | const schema = { 49 | type: 'number' 50 | } 51 | 52 | const stringify = build(schema) 53 | 54 | values.forEach(v => t.assert.equal(stringify(v), JSON.stringify(v))) 55 | }) 56 | -------------------------------------------------------------------------------- /test/integer.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | 5 | const validator = require('is-my-json-valid') 6 | const build = require('..') 7 | const ROUNDING_TYPES = ['ceil', 'floor', 'round'] 8 | 9 | test('render an integer as JSON', (t) => { 10 | t.plan(2) 11 | 12 | const schema = { 13 | title: 'integer', 14 | type: 'integer' 15 | } 16 | 17 | const validate = validator(schema) 18 | const stringify = build(schema) 19 | const output = stringify(1615) 20 | 21 | t.assert.equal(output, '1615') 22 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 23 | }) 24 | 25 | test('render a float as an integer', (t) => { 26 | t.plan(2) 27 | try { 28 | build({ 29 | title: 'float as integer', 30 | type: 'integer' 31 | }, { rounding: 'foobar' }) 32 | } catch (error) { 33 | t.assert.ok(error) 34 | t.assert.equal(error.message, 'Unsupported integer rounding method foobar') 35 | } 36 | }) 37 | 38 | test('throws on NaN', (t) => { 39 | t.plan(1) 40 | 41 | const schema = { 42 | title: 'integer', 43 | type: 'integer' 44 | } 45 | 46 | const stringify = build(schema) 47 | t.assert.throws(() => stringify(NaN), new Error('The value "NaN" cannot be converted to an integer.')) 48 | }) 49 | 50 | test('render a float as an integer', (t) => { 51 | const cases = [ 52 | { input: Math.PI, output: '3' }, 53 | { input: 5.0, output: '5' }, 54 | { input: null, output: '0' }, 55 | { input: 0, output: '0' }, 56 | { input: 0.0, output: '0' }, 57 | { input: 42, output: '42' }, 58 | { input: 1.99999, output: '1' }, 59 | { input: -45.05, output: '-45' }, 60 | { input: 3333333333333333, output: '3333333333333333' }, 61 | { input: Math.PI, output: '3', rounding: 'trunc' }, 62 | { input: 5.0, output: '5', rounding: 'trunc' }, 63 | { input: null, output: '0', rounding: 'trunc' }, 64 | { input: 0, output: '0', rounding: 'trunc' }, 65 | { input: 0.0, output: '0', rounding: 'trunc' }, 66 | { input: 42, output: '42', rounding: 'trunc' }, 67 | { input: 1.99999, output: '1', rounding: 'trunc' }, 68 | { input: -45.05, output: '-45', rounding: 'trunc' }, 69 | { input: 0.95, output: '1', rounding: 'ceil' }, 70 | { input: 0.2, output: '1', rounding: 'ceil' }, 71 | { input: 45.95, output: '45', rounding: 'floor' }, 72 | { input: -45.05, output: '-46', rounding: 'floor' }, 73 | { input: 45.44, output: '45', rounding: 'round' }, 74 | { input: 45.95, output: '46', rounding: 'round' } 75 | ] 76 | 77 | t.plan(cases.length * 2) 78 | cases.forEach(checkInteger) 79 | 80 | function checkInteger ({ input, output, rounding }) { 81 | const schema = { 82 | title: 'float as integer', 83 | type: 'integer' 84 | } 85 | 86 | const validate = validator(schema) 87 | const stringify = build(schema, { rounding }) 88 | const str = stringify(input) 89 | 90 | t.assert.equal(str, output) 91 | t.assert.ok(validate(JSON.parse(str)), 'valid schema') 92 | } 93 | }) 94 | 95 | test('render an object with an integer as JSON', (t) => { 96 | t.plan(2) 97 | 98 | const schema = { 99 | title: 'object with integer', 100 | type: 'object', 101 | properties: { 102 | id: { 103 | type: 'integer' 104 | } 105 | } 106 | } 107 | 108 | const validate = validator(schema) 109 | const stringify = build(schema) 110 | const output = stringify({ 111 | id: 1615 112 | }) 113 | 114 | t.assert.equal(output, '{"id":1615}') 115 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 116 | }) 117 | 118 | test('render an array with an integer as JSON', (t) => { 119 | t.plan(2) 120 | 121 | const schema = { 122 | title: 'array with integer', 123 | type: 'array', 124 | items: { 125 | type: 'integer' 126 | } 127 | } 128 | 129 | const validate = validator(schema) 130 | const stringify = build(schema) 131 | const output = stringify([1615]) 132 | 133 | t.assert.equal(output, '[1615]') 134 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 135 | }) 136 | 137 | test('render an object with an additionalProperty of type integer as JSON', (t) => { 138 | t.plan(2) 139 | 140 | const schema = { 141 | title: 'object with integer', 142 | type: 'object', 143 | additionalProperties: { 144 | type: 'integer' 145 | } 146 | } 147 | 148 | const validate = validator(schema) 149 | const stringify = build(schema) 150 | const output = stringify({ 151 | num: 1615 152 | }) 153 | 154 | t.assert.equal(output, '{"num":1615}') 155 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 156 | }) 157 | 158 | test('should round integer object parameter', t => { 159 | t.plan(2) 160 | 161 | const schema = { type: 'object', properties: { magic: { type: 'integer' } } } 162 | const validate = validator(schema) 163 | const stringify = build(schema, { rounding: 'ceil' }) 164 | const output = stringify({ magic: 4.2 }) 165 | 166 | t.assert.equal(output, '{"magic":5}') 167 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 168 | }) 169 | 170 | test('should not stringify a property if it does not exist', t => { 171 | t.plan(2) 172 | 173 | const schema = { title: 'Example Schema', type: 'object', properties: { age: { type: 'integer' } } } 174 | const validate = validator(schema) 175 | const stringify = build(schema) 176 | const output = stringify({}) 177 | 178 | t.assert.equal(output, '{}') 179 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 180 | }) 181 | 182 | ROUNDING_TYPES.forEach((rounding) => { 183 | test(`should not stringify a property if it does not exist (rounding: ${rounding})`, t => { 184 | t.plan(2) 185 | 186 | const schema = { type: 'object', properties: { magic: { type: 'integer' } } } 187 | const validate = validator(schema) 188 | const stringify = build(schema, { rounding }) 189 | const output = stringify({}) 190 | 191 | t.assert.equal(output, '{}') 192 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 193 | }) 194 | }) 195 | -------------------------------------------------------------------------------- /test/invalidSchema.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | // Covers issue #139 7 | test('Should throw on invalid schema', t => { 8 | t.plan(1) 9 | t.assert.throws(() => { 10 | build({}, { 11 | schema: { 12 | invalid: { 13 | type: 'Dinosaur' 14 | } 15 | } 16 | }) 17 | }, { message: /^"invalid" schema is invalid:.*/ }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/issue-479.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('should validate anyOf after allOf merge', (t) => { 7 | t.plan(1) 8 | 9 | const schema = { 10 | $id: 'schema', 11 | type: 'object', 12 | allOf: [ 13 | { 14 | $id: 'base', 15 | type: 'object', 16 | properties: { 17 | name: { 18 | type: 'string' 19 | } 20 | }, 21 | required: [ 22 | 'name' 23 | ] 24 | }, 25 | { 26 | $id: 'inner_schema', 27 | type: 'object', 28 | properties: { 29 | union: { 30 | $id: '#id', 31 | anyOf: [ 32 | { 33 | 34 | $id: 'guid', 35 | type: 'string' 36 | }, 37 | { 38 | 39 | $id: 'email', 40 | type: 'string' 41 | } 42 | ] 43 | } 44 | }, 45 | required: [ 46 | 'union' 47 | ] 48 | } 49 | ] 50 | } 51 | 52 | const stringify = build(schema) 53 | 54 | t.assert.equal( 55 | stringify({ name: 'foo', union: 'a8f1cc50-5530-5c62-9109-5ba9589a6ae1' }), 56 | '{"name":"foo","union":"a8f1cc50-5530-5c62-9109-5ba9589a6ae1"}') 57 | }) 58 | -------------------------------------------------------------------------------- /test/json-schema-test-suite/README.md: -------------------------------------------------------------------------------- 1 | # JSON-Schema-Test-Suite 2 | 3 | You can find all test cases [here](https://github.com/json-schema-org/JSON-Schema-Test-Suite). 4 | It contains a set of JSON objects that implementors of JSON Schema validation libraries can use to test their validators. 5 | 6 | # How to add another test case? 7 | 8 | 1. Navigate to [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite/tree/main/tests) 9 | 2. Choose a draft `draft4`, `draft6` or `draft7` 10 | 3. Copy & paste the `test-case.json` to the project and add a test like in the `draft4.test.js` -------------------------------------------------------------------------------- /test/json-schema-test-suite/draft4.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const { counTests, runTests } = require('./util') 5 | 6 | const requiredTestSuite = require('./draft4/required.json') 7 | 8 | test('required', async (t) => { 9 | const skippedTests = ['ignores arrays', 'ignores strings', 'ignores other non-objects'] 10 | t.plan(counTests(requiredTestSuite, skippedTests)) 11 | await runTests(t, requiredTestSuite, skippedTests) 12 | }) 13 | -------------------------------------------------------------------------------- /test/json-schema-test-suite/draft4/required.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "required validation", 4 | "schema": { 5 | "properties": { 6 | "foo": {}, 7 | "bar": {} 8 | }, 9 | "required": ["foo"] 10 | }, 11 | "tests": [ 12 | { 13 | "description": "present required property is valid", 14 | "data": {"foo": 1}, 15 | "valid": true 16 | }, 17 | { 18 | "description": "non-present required property is invalid", 19 | "data": {"bar": 1}, 20 | "valid": false 21 | }, 22 | { 23 | "description": "ignores arrays", 24 | "data": [], 25 | "valid": true 26 | }, 27 | { 28 | "description": "ignores strings", 29 | "data": "", 30 | "valid": true 31 | }, 32 | { 33 | "description": "ignores other non-objects", 34 | "data": 12, 35 | "valid": true 36 | } 37 | ] 38 | }, 39 | { 40 | "description": "required default validation", 41 | "schema": { 42 | "properties": { 43 | "foo": {} 44 | } 45 | }, 46 | "tests": [ 47 | { 48 | "description": "not required by default", 49 | "data": {}, 50 | "valid": true 51 | } 52 | ] 53 | } 54 | ] 55 | -------------------------------------------------------------------------------- /test/json-schema-test-suite/draft6.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const { counTests, runTests } = require('./util') 5 | 6 | const requiredTestSuite = require('./draft6/required.json') 7 | 8 | test('required', async (t) => { 9 | const skippedTests = ['ignores arrays', 'ignores strings', 'ignores other non-objects'] 10 | t.plan(counTests(requiredTestSuite, skippedTests)) 11 | await runTests(t, requiredTestSuite, skippedTests) 12 | }) 13 | -------------------------------------------------------------------------------- /test/json-schema-test-suite/draft6/required.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "required validation", 4 | "schema": { 5 | "properties": { 6 | "foo": {}, 7 | "bar": {} 8 | }, 9 | "required": ["foo"] 10 | }, 11 | "tests": [ 12 | { 13 | "description": "present required property is valid", 14 | "data": {"foo": 1}, 15 | "valid": true 16 | }, 17 | { 18 | "description": "non-present required property is invalid", 19 | "data": {"bar": 1}, 20 | "valid": false 21 | }, 22 | { 23 | "description": "ignores arrays", 24 | "data": [], 25 | "valid": true 26 | }, 27 | { 28 | "description": "ignores strings", 29 | "data": "", 30 | "valid": true 31 | }, 32 | { 33 | "description": "ignores other non-objects", 34 | "data": 12, 35 | "valid": true 36 | } 37 | ] 38 | }, 39 | { 40 | "description": "required default validation", 41 | "schema": { 42 | "properties": { 43 | "foo": {} 44 | } 45 | }, 46 | "tests": [ 47 | { 48 | "description": "not required by default", 49 | "data": {}, 50 | "valid": true 51 | } 52 | ] 53 | }, 54 | { 55 | "description": "required with empty array", 56 | "schema": { 57 | "properties": { 58 | "foo": {} 59 | }, 60 | "required": [] 61 | }, 62 | "tests": [ 63 | { 64 | "description": "property not required", 65 | "data": {}, 66 | "valid": true 67 | } 68 | ] 69 | } 70 | ] 71 | -------------------------------------------------------------------------------- /test/json-schema-test-suite/draft7.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const { counTests, runTests } = require('./util') 5 | 6 | const requiredTestSuite = require('./draft7/required.json') 7 | 8 | test('required', async (t) => { 9 | const skippedTests = ['ignores arrays', 'ignores strings', 'ignores other non-objects'] 10 | t.plan(counTests(requiredTestSuite, skippedTests)) 11 | await runTests(t, requiredTestSuite, skippedTests) 12 | }) 13 | -------------------------------------------------------------------------------- /test/json-schema-test-suite/draft7/required.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "required validation", 4 | "schema": { 5 | "properties": { 6 | "foo": {}, 7 | "bar": {} 8 | }, 9 | "required": ["foo"] 10 | }, 11 | "tests": [ 12 | { 13 | "description": "present required property is valid", 14 | "data": {"foo": 1}, 15 | "valid": true 16 | }, 17 | { 18 | "description": "non-present required property is invalid", 19 | "data": {"bar": 1}, 20 | "valid": false 21 | }, 22 | { 23 | "description": "ignores arrays", 24 | "data": [], 25 | "valid": true 26 | }, 27 | { 28 | "description": "ignores strings", 29 | "data": "", 30 | "valid": true 31 | }, 32 | { 33 | "description": "ignores other non-objects", 34 | "data": 12, 35 | "valid": true 36 | } 37 | ] 38 | }, 39 | { 40 | "description": "required default validation", 41 | "schema": { 42 | "properties": { 43 | "foo": {} 44 | } 45 | }, 46 | "tests": [ 47 | { 48 | "description": "not required by default", 49 | "data": {}, 50 | "valid": true 51 | } 52 | ] 53 | }, 54 | { 55 | "description": "required with empty array", 56 | "schema": { 57 | "properties": { 58 | "foo": {} 59 | }, 60 | "required": [] 61 | }, 62 | "tests": [ 63 | { 64 | "description": "property not required", 65 | "data": {}, 66 | "valid": true 67 | } 68 | ] 69 | } 70 | ] 71 | -------------------------------------------------------------------------------- /test/json-schema-test-suite/util.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const build = require('../..') 4 | 5 | async function runTests (t, testsuite, skippedTests) { 6 | for (const scenario of testsuite) { 7 | const stringify = build(scenario.schema) 8 | for (const test of scenario.tests) { 9 | if (skippedTests.indexOf(test.description) !== -1) { 10 | console.log(`skip ${test.description}`) 11 | continue 12 | } 13 | 14 | await t.test(test.description, (t) => { 15 | t.plan(1) 16 | try { 17 | const output = stringify(test.data) 18 | t.assert.equal(output, JSON.stringify(test.data), 'compare payloads') 19 | } catch (err) { 20 | t.assert.ok(test.valid === false, 'payload should be valid: ' + err.message) 21 | } 22 | }) 23 | } 24 | } 25 | } 26 | 27 | function counTests (ts, skippedTests) { 28 | return ts.reduce((a, b) => a + b.tests.length, 0) - skippedTests.length 29 | } 30 | 31 | module.exports = { runTests, counTests } 32 | -------------------------------------------------------------------------------- /test/missing-values.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('missing values', (t) => { 7 | t.plan(3) 8 | 9 | const stringify = build({ 10 | title: 'object with missing values', 11 | type: 'object', 12 | properties: { 13 | str: { 14 | type: 'string' 15 | }, 16 | num: { 17 | type: 'number' 18 | }, 19 | val: { 20 | type: 'string' 21 | } 22 | } 23 | }) 24 | 25 | t.assert.equal('{"val":"value"}', stringify({ val: 'value' })) 26 | t.assert.equal('{"str":"string","val":"value"}', stringify({ str: 'string', val: 'value' })) 27 | t.assert.equal('{"str":"string","num":42,"val":"value"}', stringify({ str: 'string', num: 42, val: 'value' })) 28 | }) 29 | 30 | test('handle null when value should be string', (t) => { 31 | t.plan(1) 32 | 33 | const stringify = build({ 34 | type: 'object', 35 | properties: { 36 | str: { 37 | type: 'string' 38 | } 39 | } 40 | }) 41 | 42 | t.assert.equal('{"str":""}', stringify({ str: null })) 43 | }) 44 | 45 | test('handle null when value should be integer', (t) => { 46 | t.plan(1) 47 | 48 | const stringify = build({ 49 | type: 'object', 50 | properties: { 51 | int: { 52 | type: 'integer' 53 | } 54 | } 55 | }) 56 | 57 | t.assert.equal('{"int":0}', stringify({ int: null })) 58 | }) 59 | 60 | test('handle null when value should be number', (t) => { 61 | t.plan(1) 62 | 63 | const stringify = build({ 64 | type: 'object', 65 | properties: { 66 | num: { 67 | type: 'number' 68 | } 69 | } 70 | }) 71 | 72 | t.assert.equal('{"num":0}', stringify({ num: null })) 73 | }) 74 | 75 | test('handle null when value should be boolean', (t) => { 76 | t.plan(1) 77 | 78 | const stringify = build({ 79 | type: 'object', 80 | properties: { 81 | bool: { 82 | type: 'boolean' 83 | } 84 | } 85 | }) 86 | 87 | t.assert.equal('{"bool":false}', stringify({ bool: null })) 88 | }) 89 | -------------------------------------------------------------------------------- /test/multi-type-serializer.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('should throw a TypeError with the path to the key of the invalid value', (t) => { 7 | t.plan(1) 8 | const schema = { 9 | type: 'object', 10 | properties: { 11 | num: { 12 | type: ['number'] 13 | } 14 | } 15 | } 16 | 17 | const stringify = build(schema) 18 | t.assert.throws(() => stringify({ num: { bla: 123 } }), new TypeError('The value of \'#/properties/num\' does not match schema definition.')) 19 | }) 20 | -------------------------------------------------------------------------------- /test/nestedObjects.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('nested objects with same properties', (t) => { 7 | t.plan(1) 8 | 9 | const schema = { 10 | title: 'nested objects with same properties', 11 | type: 'object', 12 | properties: { 13 | stringProperty: { 14 | type: 'string' 15 | }, 16 | objectProperty: { 17 | type: 'object', 18 | additionalProperties: true 19 | } 20 | } 21 | } 22 | const stringify = build(schema) 23 | 24 | const value = stringify({ 25 | stringProperty: 'string1', 26 | objectProperty: { 27 | stringProperty: 'string2', 28 | numberProperty: 42 29 | } 30 | }) 31 | t.assert.equal(value, '{"stringProperty":"string1","objectProperty":{"stringProperty":"string2","numberProperty":42}}') 32 | }) 33 | 34 | test('names collision', (t) => { 35 | t.plan(1) 36 | 37 | const schema = { 38 | title: 'nested objects with same properties', 39 | type: 'object', 40 | properties: { 41 | test: { 42 | type: 'object', 43 | properties: { 44 | a: { type: 'string' } 45 | } 46 | }, 47 | tes: { 48 | type: 'object', 49 | properties: { 50 | b: { type: 'string' }, 51 | t: { type: 'object' } 52 | } 53 | } 54 | } 55 | } 56 | const stringify = build(schema) 57 | const data = { 58 | test: { a: 'a' }, 59 | tes: { b: 'b', t: {} } 60 | } 61 | 62 | t.assert.equal(stringify(data), JSON.stringify(data)) 63 | }) 64 | -------------------------------------------------------------------------------- /test/nullable.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | 5 | const build = require('..') 6 | 7 | const nullable = true 8 | 9 | const complexObject = { 10 | type: 'object', 11 | properties: { 12 | nullableString: { type: 'string', nullable }, 13 | nullableNumber: { type: 'number', nullable }, 14 | nullableInteger: { type: 'integer', nullable }, 15 | nullableBoolean: { type: 'boolean', nullable }, 16 | nullableNull: { type: 'null', nullable }, 17 | nullableArray: { 18 | type: 'array', 19 | nullable: true, 20 | items: {} 21 | }, 22 | nullableObject: { type: 'object', nullable: true }, 23 | objectWithNullableProps: { 24 | type: 'object', 25 | nullable: false, 26 | additionalProperties: true, 27 | properties: { 28 | nullableString: { type: 'string', nullable }, 29 | nullableNumber: { type: 'number', nullable }, 30 | nullableInteger: { type: 'integer', nullable }, 31 | nullableBoolean: { type: 'boolean', nullable }, 32 | nullableNull: { type: 'null', nullable }, 33 | nullableArray: { 34 | type: 'array', 35 | nullable: true, 36 | items: {} 37 | } 38 | } 39 | }, 40 | arrayWithNullableItems: { 41 | type: 'array', 42 | nullable: true, 43 | items: { type: ['integer', 'string'], nullable: true } 44 | } 45 | } 46 | } 47 | 48 | const complexData = { 49 | nullableString: null, 50 | nullableNumber: null, 51 | nullableInteger: null, 52 | nullableBoolean: null, 53 | nullableNull: null, 54 | nullableArray: null, 55 | nullableObject: null, 56 | objectWithNullableProps: { 57 | additionalProp: null, 58 | nullableString: null, 59 | nullableNumber: null, 60 | nullableInteger: null, 61 | nullableBoolean: null, 62 | nullableNull: null, 63 | nullableArray: null 64 | }, 65 | arrayWithNullableItems: [1, 2, null] 66 | } 67 | 68 | const complexExpectedResult = { 69 | nullableString: null, 70 | nullableNumber: null, 71 | nullableInteger: null, 72 | nullableBoolean: null, 73 | nullableNull: null, 74 | nullableArray: null, 75 | nullableObject: null, 76 | objectWithNullableProps: { 77 | additionalProp: null, 78 | nullableString: null, 79 | nullableNumber: null, 80 | nullableInteger: null, 81 | nullableBoolean: null, 82 | nullableNull: null, 83 | nullableArray: null 84 | }, 85 | arrayWithNullableItems: [1, 2, null] 86 | } 87 | 88 | const testSet = { 89 | nullableString: [{ type: 'string', nullable }, null, null], 90 | nullableNumber: [{ type: 'number', nullable }, null, null], 91 | nullableInteger: [{ type: 'integer', nullable }, null, null], 92 | nullableBoolean: [{ type: 'boolean', nullable }, null, null], 93 | nullableNull: [{ type: 'null', nullable }, null, null], 94 | nullableArray: [{ 95 | type: 'array', 96 | nullable: true, 97 | items: {} 98 | }, null, null], 99 | nullableObject: [{ type: 'object', nullable: true }, null, null], 100 | complexObject: [complexObject, complexData, complexExpectedResult, { ajv: { allowUnionTypes: true } }] 101 | } 102 | 103 | Object.keys(testSet).forEach(key => { 104 | test(`handle nullable:true in ${key} correctly`, (t) => { 105 | t.plan(1) 106 | 107 | const [ 108 | schema, 109 | data, 110 | expected, 111 | extraOptions 112 | ] = testSet[key] 113 | 114 | const stringifier = build(schema, extraOptions) 115 | const result = stringifier(data) 116 | t.assert.deepStrictEqual(JSON.parse(result), expected) 117 | }) 118 | }) 119 | 120 | test('handle nullable number correctly', (t) => { 121 | t.plan(2) 122 | 123 | const schema = { 124 | type: 'number', 125 | nullable: true 126 | } 127 | const stringify = build(schema) 128 | 129 | const data = null 130 | const result = stringify(data) 131 | 132 | t.assert.equal(result, JSON.stringify(data)) 133 | t.assert.equal(JSON.parse(result), data) 134 | }) 135 | 136 | test('handle nullable integer correctly', (t) => { 137 | t.plan(2) 138 | 139 | const schema = { 140 | type: 'integer', 141 | nullable: true 142 | } 143 | const stringify = build(schema) 144 | 145 | const data = null 146 | const result = stringify(data) 147 | 148 | t.assert.equal(result, JSON.stringify(data)) 149 | t.assert.equal(JSON.parse(result), data) 150 | }) 151 | 152 | test('handle nullable boolean correctly', (t) => { 153 | t.plan(2) 154 | 155 | const schema = { 156 | type: 'boolean', 157 | nullable: true 158 | } 159 | const stringify = build(schema) 160 | 161 | const data = null 162 | const result = stringify(data) 163 | 164 | t.assert.equal(result, JSON.stringify(data)) 165 | t.assert.equal(JSON.parse(result), data) 166 | }) 167 | 168 | test('handle nullable string correctly', (t) => { 169 | t.plan(2) 170 | 171 | const schema = { 172 | type: 'string', 173 | nullable: true 174 | } 175 | const stringify = build(schema) 176 | 177 | const data = null 178 | const result = stringify(data) 179 | 180 | t.assert.equal(result, JSON.stringify(data)) 181 | t.assert.equal(JSON.parse(result), data) 182 | }) 183 | 184 | test('handle nullable date-time correctly', (t) => { 185 | t.plan(2) 186 | 187 | const schema = { 188 | type: 'string', 189 | format: 'date-time', 190 | nullable: true 191 | } 192 | const stringify = build(schema) 193 | 194 | const data = null 195 | const result = stringify(data) 196 | 197 | t.assert.equal(result, JSON.stringify(data)) 198 | t.assert.equal(JSON.parse(result), data) 199 | }) 200 | 201 | test('handle nullable date correctly', (t) => { 202 | t.plan(2) 203 | 204 | const schema = { 205 | type: 'string', 206 | format: 'date', 207 | nullable: true 208 | } 209 | const stringify = build(schema) 210 | 211 | const data = null 212 | const result = stringify(data) 213 | 214 | t.assert.equal(result, JSON.stringify(data)) 215 | t.assert.equal(JSON.parse(result), data) 216 | }) 217 | 218 | test('handle nullable time correctly', (t) => { 219 | t.plan(2) 220 | 221 | const schema = { 222 | type: 'string', 223 | format: 'time', 224 | nullable: true 225 | } 226 | const stringify = build(schema) 227 | 228 | const data = null 229 | const result = stringify(data) 230 | 231 | t.assert.equal(result, JSON.stringify(data)) 232 | t.assert.equal(JSON.parse(result), data) 233 | }) 234 | 235 | test('large array of nullable strings with default mechanism', (t) => { 236 | t.plan(2) 237 | 238 | const schema = { 239 | type: 'object', 240 | properties: { 241 | ids: { 242 | type: 'array', 243 | items: { 244 | type: 'string', 245 | nullable: true 246 | } 247 | } 248 | } 249 | } 250 | 251 | const options = { 252 | largeArraySize: 2e4, 253 | largeArrayMechanism: 'default' 254 | } 255 | 256 | const stringify = build(schema, options) 257 | 258 | const data = { ids: new Array(2e4).fill(null) } 259 | const result = stringify(data) 260 | 261 | t.assert.equal(result, JSON.stringify(data)) 262 | t.assert.deepStrictEqual(JSON.parse(result), data) 263 | }) 264 | 265 | test('large array of nullable date-time strings with default mechanism', (t) => { 266 | t.plan(2) 267 | 268 | const schema = { 269 | type: 'object', 270 | properties: { 271 | ids: { 272 | type: 'array', 273 | items: { 274 | type: 'string', 275 | format: 'date-time', 276 | nullable: true 277 | } 278 | } 279 | } 280 | } 281 | 282 | const options = { 283 | largeArraySize: 2e4, 284 | largeArrayMechanism: 'default' 285 | } 286 | 287 | const stringify = build(schema, options) 288 | 289 | const data = { ids: new Array(2e4).fill(null) } 290 | const result = stringify(data) 291 | 292 | t.assert.equal(result, JSON.stringify(data)) 293 | t.assert.deepStrictEqual(JSON.parse(result), data) 294 | }) 295 | 296 | test('large array of nullable date-time strings with default mechanism', (t) => { 297 | t.plan(2) 298 | 299 | const schema = { 300 | type: 'object', 301 | properties: { 302 | ids: { 303 | type: 'array', 304 | items: { 305 | type: 'string', 306 | format: 'date', 307 | nullable: true 308 | } 309 | } 310 | } 311 | } 312 | 313 | const options = { 314 | largeArraySize: 2e4, 315 | largeArrayMechanism: 'default' 316 | } 317 | 318 | const stringify = build(schema, options) 319 | 320 | const data = { ids: new Array(2e4).fill(null) } 321 | const result = stringify(data) 322 | 323 | t.assert.equal(result, JSON.stringify(data)) 324 | t.assert.deepStrictEqual(JSON.parse(result), data) 325 | }) 326 | 327 | test('large array of nullable date-time strings with default mechanism', (t) => { 328 | t.plan(2) 329 | 330 | const schema = { 331 | type: 'object', 332 | properties: { 333 | ids: { 334 | type: 'array', 335 | items: { 336 | type: 'string', 337 | format: 'time', 338 | nullable: true 339 | } 340 | } 341 | } 342 | } 343 | 344 | const options = { 345 | largeArraySize: 2e4, 346 | largeArrayMechanism: 'default' 347 | } 348 | 349 | const stringify = build(schema, options) 350 | 351 | const data = { ids: new Array(2e4).fill(null) } 352 | const result = stringify(data) 353 | 354 | t.assert.equal(result, JSON.stringify(data)) 355 | t.assert.deepStrictEqual(JSON.parse(result), data) 356 | }) 357 | 358 | test('large array of nullable numbers with default mechanism', (t) => { 359 | t.plan(2) 360 | 361 | const schema = { 362 | type: 'object', 363 | properties: { 364 | ids: { 365 | type: 'array', 366 | items: { 367 | type: 'number', 368 | nullable: true 369 | } 370 | } 371 | } 372 | } 373 | 374 | const options = { 375 | largeArraySize: 2e4, 376 | largeArrayMechanism: 'default' 377 | } 378 | 379 | const stringify = build(schema, options) 380 | 381 | const data = { ids: new Array(2e4).fill(null) } 382 | const result = stringify(data) 383 | 384 | t.assert.equal(result, JSON.stringify(data)) 385 | t.assert.deepStrictEqual(JSON.parse(result), data) 386 | }) 387 | 388 | test('large array of nullable integers with default mechanism', (t) => { 389 | t.plan(2) 390 | 391 | const schema = { 392 | type: 'object', 393 | properties: { 394 | ids: { 395 | type: 'array', 396 | items: { 397 | type: 'integer', 398 | nullable: true 399 | } 400 | } 401 | } 402 | } 403 | 404 | const options = { 405 | largeArraySize: 2e4, 406 | largeArrayMechanism: 'default' 407 | } 408 | 409 | const stringify = build(schema, options) 410 | 411 | const data = { ids: new Array(2e4).fill(null) } 412 | const result = stringify(data) 413 | 414 | t.assert.equal(result, JSON.stringify(data)) 415 | t.assert.deepStrictEqual(JSON.parse(result), data) 416 | }) 417 | 418 | test('large array of nullable booleans with default mechanism', (t) => { 419 | t.plan(2) 420 | 421 | const schema = { 422 | type: 'object', 423 | properties: { 424 | ids: { 425 | type: 'array', 426 | items: { 427 | type: 'boolean', 428 | nullable: true 429 | } 430 | } 431 | } 432 | } 433 | 434 | const options = { 435 | largeArraySize: 2e4, 436 | largeArrayMechanism: 'default' 437 | } 438 | 439 | const stringify = build(schema, options) 440 | 441 | const data = { ids: new Array(2e4).fill(null) } 442 | const result = stringify(data) 443 | 444 | t.assert.equal(result, JSON.stringify(data)) 445 | t.assert.deepStrictEqual(JSON.parse(result), data) 446 | }) 447 | 448 | test('nullable type in the schema', (t) => { 449 | t.plan(2) 450 | 451 | const schema = { 452 | type: ['object', 'null'], 453 | properties: { 454 | foo: { 455 | type: 'string' 456 | } 457 | } 458 | } 459 | 460 | const stringify = build(schema) 461 | 462 | const data = { foo: 'bar' } 463 | 464 | t.assert.equal(stringify(data), JSON.stringify(data)) 465 | t.assert.equal(stringify(null), JSON.stringify(null)) 466 | }) 467 | 468 | test('throw an error if the value doesn\'t match the type', (t) => { 469 | t.plan(2) 470 | 471 | const schema = { 472 | type: 'object', 473 | additionalProperties: false, 474 | required: ['data'], 475 | properties: { 476 | data: { 477 | type: 'array', 478 | minItems: 1, 479 | items: { 480 | oneOf: [ 481 | { 482 | type: 'string' 483 | }, 484 | { 485 | type: 'number' 486 | } 487 | ] 488 | } 489 | } 490 | } 491 | } 492 | 493 | const stringify = build(schema) 494 | 495 | const validData = { data: [1, 'testing'] } 496 | t.assert.equal(stringify(validData), JSON.stringify(validData)) 497 | 498 | const invalidData = { data: [false, 'testing'] } 499 | t.assert.throws(() => stringify(invalidData)) 500 | }) 501 | 502 | test('nullable value in oneOf', (t) => { 503 | t.plan(1) 504 | 505 | const schema = { 506 | type: 'object', 507 | properties: { 508 | data: { 509 | oneOf: [ 510 | { 511 | type: 'array', 512 | items: { 513 | type: 'object', 514 | properties: { 515 | id: { type: 'integer', minimum: 1 } 516 | }, 517 | additionalProperties: false, 518 | required: ['id'] 519 | } 520 | }, 521 | { 522 | type: 'array', 523 | items: { 524 | type: 'object', 525 | properties: { 526 | job: { type: 'string', nullable: true } 527 | }, 528 | additionalProperties: false, 529 | required: ['job'] 530 | } 531 | } 532 | ] 533 | } 534 | }, 535 | required: ['data'], 536 | additionalProperties: false 537 | } 538 | 539 | const stringify = build(schema) 540 | 541 | const data = { data: [{ job: null }] } 542 | t.assert.equal(stringify(data), JSON.stringify(data)) 543 | }) 544 | -------------------------------------------------------------------------------- /test/oneof.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('object with multiple types field', (t) => { 7 | t.plan(2) 8 | 9 | const schema = { 10 | title: 'object with multiple types field', 11 | type: 'object', 12 | properties: { 13 | str: { 14 | oneOf: [{ 15 | type: 'string' 16 | }, { 17 | type: 'boolean' 18 | }] 19 | } 20 | } 21 | } 22 | const stringify = build(schema) 23 | 24 | t.assert.equal(stringify({ str: 'string' }), '{"str":"string"}') 25 | t.assert.equal(stringify({ str: true }), '{"str":true}') 26 | }) 27 | 28 | test('object with field of type object or null', (t) => { 29 | t.plan(2) 30 | 31 | const schema = { 32 | title: 'object with field of type object or null', 33 | type: 'object', 34 | properties: { 35 | prop: { 36 | oneOf: [{ 37 | type: 'object', 38 | properties: { 39 | str: { 40 | type: 'string' 41 | } 42 | } 43 | }, { 44 | type: 'null' 45 | }] 46 | } 47 | } 48 | } 49 | const stringify = build(schema) 50 | 51 | t.assert.equal(stringify({ prop: null }), '{"prop":null}') 52 | 53 | t.assert.equal(stringify({ 54 | prop: { 55 | str: 'string', remove: 'this' 56 | } 57 | }), '{"prop":{"str":"string"}}') 58 | }) 59 | 60 | test('object with field of type object or array', (t) => { 61 | t.plan(2) 62 | 63 | const schema = { 64 | title: 'object with field of type object or array', 65 | type: 'object', 66 | properties: { 67 | prop: { 68 | oneOf: [{ 69 | type: 'object', 70 | properties: {}, 71 | additionalProperties: true 72 | }, { 73 | type: 'array', 74 | items: { 75 | type: 'string' 76 | } 77 | }] 78 | } 79 | } 80 | } 81 | const stringify = build(schema) 82 | 83 | t.assert.equal(stringify({ 84 | prop: { str: 'string' } 85 | }), '{"prop":{"str":"string"}}') 86 | 87 | t.assert.equal(stringify({ 88 | prop: ['string'] 89 | }), '{"prop":["string"]}') 90 | }) 91 | 92 | test('object with field of type string and coercion disable ', (t) => { 93 | t.plan(1) 94 | 95 | const schema = { 96 | title: 'object with field of type string', 97 | type: 'object', 98 | properties: { 99 | str: { 100 | oneOf: [{ 101 | type: 'string' 102 | }] 103 | } 104 | } 105 | } 106 | const stringify = build(schema) 107 | t.assert.throws(() => stringify({ str: 1 })) 108 | }) 109 | 110 | test('object with field of type string and coercion enable ', (t) => { 111 | t.plan(1) 112 | 113 | const schema = { 114 | title: 'object with field of type string', 115 | type: 'object', 116 | properties: { 117 | str: { 118 | oneOf: [{ 119 | type: 'string' 120 | }] 121 | } 122 | } 123 | } 124 | 125 | const options = { 126 | ajv: { 127 | coerceTypes: true 128 | } 129 | } 130 | const stringify = build(schema, options) 131 | 132 | const value = stringify({ 133 | str: 1 134 | }) 135 | t.assert.equal(value, '{"str":"1"}') 136 | }) 137 | 138 | test('object with field with type union of multiple objects', (t) => { 139 | t.plan(2) 140 | 141 | const schema = { 142 | title: 'object with oneOf property value containing objects', 143 | type: 'object', 144 | properties: { 145 | oneOfSchema: { 146 | oneOf: [ 147 | { 148 | type: 'object', 149 | properties: { 150 | baz: { type: 'number' } 151 | }, 152 | required: ['baz'] 153 | }, 154 | { 155 | type: 'object', 156 | properties: { 157 | bar: { type: 'string' } 158 | }, 159 | required: ['bar'] 160 | } 161 | ] 162 | } 163 | }, 164 | required: ['oneOfSchema'] 165 | } 166 | 167 | const stringify = build(schema) 168 | 169 | t.assert.equal(stringify({ oneOfSchema: { baz: 5 } }), '{"oneOfSchema":{"baz":5}}') 170 | 171 | t.assert.equal(stringify({ oneOfSchema: { bar: 'foo' } }), '{"oneOfSchema":{"bar":"foo"}}') 172 | }) 173 | 174 | test('null value in schema', (t) => { 175 | t.plan(0) 176 | 177 | const schema = { 178 | title: 'schema with null child', 179 | type: 'string', 180 | nullable: true, 181 | enum: [null] 182 | } 183 | 184 | build(schema) 185 | }) 186 | 187 | test('oneOf and $ref together', (t) => { 188 | t.plan(2) 189 | 190 | const schema = { 191 | type: 'object', 192 | properties: { 193 | cs: { 194 | oneOf: [ 195 | { 196 | $ref: '#/definitions/Option' 197 | }, 198 | { 199 | type: 'boolean' 200 | } 201 | ] 202 | } 203 | }, 204 | definitions: { 205 | Option: { 206 | type: 'string' 207 | } 208 | } 209 | } 210 | 211 | const stringify = build(schema) 212 | 213 | t.assert.equal(stringify({ cs: 'franco' }), '{"cs":"franco"}') 214 | 215 | t.assert.equal(stringify({ cs: true }), '{"cs":true}') 216 | }) 217 | 218 | test('oneOf and $ref: 2 levels are fine', (t) => { 219 | t.plan(1) 220 | 221 | const schema = { 222 | type: 'object', 223 | properties: { 224 | cs: { 225 | oneOf: [ 226 | { 227 | $ref: '#/definitions/Option' 228 | }, 229 | { 230 | type: 'boolean' 231 | } 232 | ] 233 | } 234 | }, 235 | definitions: { 236 | Option: { 237 | oneOf: [ 238 | { 239 | type: 'number' 240 | }, 241 | { 242 | type: 'boolean' 243 | } 244 | ] 245 | } 246 | } 247 | } 248 | 249 | const stringify = build(schema) 250 | const value = stringify({ 251 | cs: 3 252 | }) 253 | t.assert.equal(value, '{"cs":3}') 254 | }) 255 | 256 | test('oneOf and $ref: multiple levels should throw at build.', (t) => { 257 | t.plan(3) 258 | 259 | const schema = { 260 | type: 'object', 261 | properties: { 262 | cs: { 263 | oneOf: [ 264 | { 265 | $ref: '#/definitions/Option' 266 | }, 267 | { 268 | type: 'boolean' 269 | } 270 | ] 271 | } 272 | }, 273 | definitions: { 274 | Option: { 275 | oneOf: [ 276 | { 277 | $ref: '#/definitions/Option2' 278 | }, 279 | { 280 | type: 'string' 281 | } 282 | ] 283 | }, 284 | Option2: { 285 | type: 'number' 286 | } 287 | } 288 | } 289 | 290 | const stringify = build(schema) 291 | 292 | t.assert.equal(stringify({ cs: 3 }), '{"cs":3}') 293 | t.assert.equal(stringify({ cs: true }), '{"cs":true}') 294 | t.assert.equal(stringify({ cs: 'pippo' }), '{"cs":"pippo"}') 295 | }) 296 | 297 | test('oneOf and $ref - multiple external $ref', (t) => { 298 | t.plan(2) 299 | 300 | const externalSchema = { 301 | external: { 302 | definitions: { 303 | def: { 304 | type: 'object', 305 | properties: { 306 | prop: { oneOf: [{ $ref: 'external2#/definitions/other' }] } 307 | } 308 | } 309 | } 310 | }, 311 | external2: { 312 | definitions: { 313 | internal: { 314 | type: 'string' 315 | }, 316 | other: { 317 | type: 'object', 318 | properties: { 319 | prop2: { $ref: '#/definitions/internal' } 320 | } 321 | } 322 | } 323 | } 324 | } 325 | 326 | const schema = { 327 | title: 'object with $ref', 328 | type: 'object', 329 | properties: { 330 | obj: { 331 | $ref: 'external#/definitions/def' 332 | } 333 | } 334 | } 335 | 336 | const object = { 337 | obj: { 338 | prop: { 339 | prop2: 'test' 340 | } 341 | } 342 | } 343 | 344 | const stringify = build(schema, { schema: externalSchema }) 345 | const output = stringify(object) 346 | 347 | t.assert.doesNotThrow(() => JSON.parse(output)) 348 | t.assert.equal(output, '{"obj":{"prop":{"prop2":"test"}}}') 349 | }) 350 | 351 | test('oneOf with enum with more than 100 entries', (t) => { 352 | t.plan(1) 353 | 354 | const schema = { 355 | title: 'type array that may have one of declared items', 356 | type: 'array', 357 | items: { 358 | oneOf: [ 359 | { 360 | type: 'string', 361 | enum: ['EUR', 'USD', ...(new Set([...new Array(200)].map(() => Math.random().toString(36).substr(2, 3)))).values()] 362 | }, 363 | { type: 'null' } 364 | ] 365 | } 366 | } 367 | const stringify = build(schema) 368 | 369 | const value = stringify(['EUR', 'USD', null]) 370 | t.assert.equal(value, '["EUR","USD",null]') 371 | }) 372 | 373 | test('oneOf object with field of type string with format or null', (t) => { 374 | t.plan(1) 375 | 376 | const toStringify = new Date() 377 | 378 | const withOneOfSchema = { 379 | type: 'object', 380 | properties: { 381 | prop: { 382 | oneOf: [{ 383 | type: 'string', 384 | format: 'date-time' 385 | }, { 386 | type: 'null' 387 | }] 388 | } 389 | } 390 | } 391 | 392 | const withOneOfStringify = build(withOneOfSchema) 393 | 394 | t.assert.equal(withOneOfStringify({ 395 | prop: toStringify 396 | }), `{"prop":"${toStringify.toISOString()}"}`) 397 | }) 398 | 399 | test('one array item match oneOf types', (t) => { 400 | t.plan(3) 401 | 402 | const schema = { 403 | type: 'object', 404 | additionalProperties: false, 405 | required: ['data'], 406 | properties: { 407 | data: { 408 | type: 'array', 409 | minItems: 1, 410 | items: { 411 | oneOf: [ 412 | { 413 | type: 'string' 414 | }, 415 | { 416 | type: 'number' 417 | } 418 | ] 419 | } 420 | } 421 | } 422 | } 423 | 424 | const stringify = build(schema) 425 | 426 | t.assert.equal(stringify({ data: ['foo'] }), '{"data":["foo"]}') 427 | t.assert.equal(stringify({ data: [1] }), '{"data":[1]}') 428 | t.assert.throws(() => stringify({ data: [false, 'foo'] })) 429 | }) 430 | 431 | test('some array items match oneOf types', (t) => { 432 | t.plan(2) 433 | 434 | const schema = { 435 | type: 'object', 436 | additionalProperties: false, 437 | required: ['data'], 438 | properties: { 439 | data: { 440 | type: 'array', 441 | minItems: 1, 442 | items: { 443 | oneOf: [ 444 | { 445 | type: 'string' 446 | }, 447 | { 448 | type: 'number' 449 | } 450 | ] 451 | } 452 | } 453 | } 454 | } 455 | 456 | const stringify = build(schema) 457 | 458 | t.assert.equal(stringify({ data: ['foo', 5] }), '{"data":["foo",5]}') 459 | t.assert.throws(() => stringify({ data: [false, 'foo', true, 5] })) 460 | }) 461 | 462 | test('all array items does not match oneOf types', (t) => { 463 | t.plan(1) 464 | 465 | const schema = { 466 | type: 'object', 467 | additionalProperties: false, 468 | required: ['data'], 469 | properties: { 470 | data: { 471 | type: 'array', 472 | minItems: 1, 473 | items: { 474 | oneOf: [ 475 | { 476 | type: 'string' 477 | }, 478 | { 479 | type: 'number' 480 | } 481 | ] 482 | } 483 | } 484 | } 485 | } 486 | 487 | const stringify = build(schema) 488 | 489 | t.assert.throws(() => stringify({ data: [null, false, true, undefined, [], {}] })) 490 | }) 491 | -------------------------------------------------------------------------------- /test/patternProperties.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('patternProperties', (t) => { 7 | t.plan(1) 8 | const stringify = build({ 9 | title: 'patternProperties', 10 | type: 'object', 11 | properties: { 12 | str: { 13 | type: 'string' 14 | } 15 | }, 16 | patternProperties: { 17 | foo: { 18 | type: 'string' 19 | } 20 | } 21 | }) 22 | 23 | const obj = { str: 'test', foo: 42, ofoo: true, foof: 'string', objfoo: { a: true }, notMe: false } 24 | t.assert.equal(stringify(obj), '{"str":"test","foo":"42","ofoo":"true","foof":"string","objfoo":"[object Object]"}') 25 | }) 26 | 27 | test('patternProperties should not change properties', (t) => { 28 | t.plan(1) 29 | const stringify = build({ 30 | title: 'patternProperties should not change properties', 31 | type: 'object', 32 | properties: { 33 | foo: { 34 | type: 'string' 35 | } 36 | }, 37 | patternProperties: { 38 | foo: { 39 | type: 'number' 40 | } 41 | } 42 | }) 43 | 44 | const obj = { foo: '42', ofoo: 42 } 45 | t.assert.equal(stringify(obj), '{"foo":"42","ofoo":42}') 46 | }) 47 | 48 | test('patternProperties - string coerce', (t) => { 49 | t.plan(1) 50 | const stringify = build({ 51 | title: 'check string coerce', 52 | type: 'object', 53 | properties: {}, 54 | patternProperties: { 55 | foo: { 56 | type: 'string' 57 | } 58 | } 59 | }) 60 | 61 | const obj = { foo: true, ofoo: 42, arrfoo: ['array', 'test'], objfoo: { a: 'world' } } 62 | t.assert.equal(stringify(obj), '{"foo":"true","ofoo":"42","arrfoo":"array,test","objfoo":"[object Object]"}') 63 | }) 64 | 65 | test('patternProperties - number coerce', (t) => { 66 | t.plan(2) 67 | const stringify = build({ 68 | title: 'check number coerce', 69 | type: 'object', 70 | properties: {}, 71 | patternProperties: { 72 | foo: { 73 | type: 'number' 74 | } 75 | } 76 | }) 77 | 78 | const coercibleValues = { foo: true, ofoo: '42' } 79 | t.assert.equal(stringify(coercibleValues), '{"foo":1,"ofoo":42}') 80 | 81 | const incoercibleValues = { xfoo: 'string', arrfoo: [1, 2], objfoo: { num: 42 } } 82 | try { 83 | stringify(incoercibleValues) 84 | t.fail('should throw an error') 85 | } catch (err) { 86 | t.assert.ok(err) 87 | } 88 | }) 89 | 90 | test('patternProperties - boolean coerce', (t) => { 91 | t.plan(1) 92 | const stringify = build({ 93 | title: 'check boolean coerce', 94 | type: 'object', 95 | properties: {}, 96 | patternProperties: { 97 | foo: { 98 | type: 'boolean' 99 | } 100 | } 101 | }) 102 | 103 | const obj = { foo: 'true', ofoo: 0, arrfoo: [1, 2], objfoo: { a: true } } 104 | t.assert.equal(stringify(obj), '{"foo":true,"ofoo":false,"arrfoo":true,"objfoo":true}') 105 | }) 106 | 107 | test('patternProperties - object coerce', (t) => { 108 | t.plan(1) 109 | const stringify = build({ 110 | title: 'check object coerce', 111 | type: 'object', 112 | properties: {}, 113 | patternProperties: { 114 | foo: { 115 | type: 'object', 116 | properties: { 117 | answer: { 118 | type: 'number' 119 | } 120 | } 121 | } 122 | } 123 | }) 124 | 125 | const obj = { objfoo: { answer: 42 } } 126 | t.assert.equal(stringify(obj), '{"objfoo":{"answer":42}}') 127 | }) 128 | 129 | test('patternProperties - array coerce', (t) => { 130 | t.plan(2) 131 | const stringify = build({ 132 | title: 'check array coerce', 133 | type: 'object', 134 | properties: {}, 135 | patternProperties: { 136 | foo: { 137 | type: 'array', 138 | items: { 139 | type: 'string' 140 | } 141 | } 142 | } 143 | }) 144 | 145 | const coercibleValues = { arrfoo: [1, 2] } 146 | t.assert.equal(stringify(coercibleValues), '{"arrfoo":["1","2"]}') 147 | 148 | const incoercibleValues = { foo: 'true', ofoo: 0, objfoo: { tyrion: 'lannister' } } 149 | t.assert.throws(() => stringify(incoercibleValues)) 150 | }) 151 | 152 | test('patternProperties - fail on invalid regex, handled by ajv', (t) => { 153 | t.plan(1) 154 | 155 | t.assert.throws(() => build({ 156 | title: 'check array coerce', 157 | type: 'object', 158 | properties: {}, 159 | patternProperties: { 160 | 'foo/\\': { 161 | type: 'array', 162 | items: { 163 | type: 'string' 164 | } 165 | } 166 | } 167 | }), new Error('schema is invalid: data/patternProperties must match format "regex"')) 168 | }) 169 | -------------------------------------------------------------------------------- /test/recursion.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('can stringify recursive directory tree (issue #181)', (t) => { 7 | t.plan(1) 8 | 9 | const schema = { 10 | definitions: { 11 | directory: { 12 | type: 'object', 13 | properties: { 14 | name: { type: 'string' }, 15 | subDirectories: { 16 | type: 'array', 17 | items: { $ref: '#/definitions/directory' }, 18 | default: [] 19 | } 20 | } 21 | } 22 | }, 23 | type: 'array', 24 | items: { $ref: '#/definitions/directory' } 25 | } 26 | const stringify = build(schema) 27 | 28 | t.assert.equal(stringify([ 29 | { name: 'directory 1', subDirectories: [] }, 30 | { 31 | name: 'directory 2', 32 | subDirectories: [ 33 | { name: 'directory 2.1', subDirectories: [] }, 34 | { name: 'directory 2.2', subDirectories: [] } 35 | ] 36 | } 37 | ]), '[{"name":"directory 1","subDirectories":[]},{"name":"directory 2","subDirectories":[{"name":"directory 2.1","subDirectories":[]},{"name":"directory 2.2","subDirectories":[]}]}]') 38 | }) 39 | 40 | test('can stringify when recursion in external schema', t => { 41 | t.plan(1) 42 | 43 | const referenceSchema = { 44 | $id: 'person', 45 | type: 'object', 46 | properties: { 47 | name: { type: 'string' }, 48 | children: { 49 | type: 'array', 50 | items: { $ref: '#' } 51 | } 52 | } 53 | } 54 | 55 | const schema = { 56 | $id: 'mainSchema', 57 | type: 'object', 58 | properties: { 59 | people: { 60 | $ref: 'person' 61 | } 62 | } 63 | } 64 | const stringify = build(schema, { 65 | schema: { 66 | [referenceSchema.$id]: referenceSchema 67 | } 68 | }) 69 | 70 | const value = stringify({ people: { name: 'Elizabeth', children: [{ name: 'Charles' }] } }) 71 | t.assert.equal(value, '{"people":{"name":"Elizabeth","children":[{"name":"Charles"}]}}') 72 | }) 73 | 74 | test('use proper serialize function', t => { 75 | t.plan(1) 76 | 77 | const personSchema = { 78 | $id: 'person', 79 | type: 'object', 80 | properties: { 81 | name: { type: 'string' }, 82 | children: { 83 | type: 'array', 84 | items: { $ref: '#' } 85 | } 86 | } 87 | } 88 | 89 | const directorySchema = { 90 | $id: 'directory', 91 | type: 'object', 92 | properties: { 93 | name: { type: 'string' }, 94 | subDirectories: { 95 | type: 'array', 96 | items: { $ref: '#' }, 97 | default: [] 98 | } 99 | } 100 | } 101 | 102 | const schema = { 103 | $id: 'mainSchema', 104 | type: 'object', 105 | properties: { 106 | people: { $ref: 'person' }, 107 | directory: { $ref: 'directory' } 108 | } 109 | } 110 | const stringify = build(schema, { 111 | schema: { 112 | [personSchema.$id]: personSchema, 113 | [directorySchema.$id]: directorySchema 114 | } 115 | }) 116 | 117 | const value = stringify({ 118 | people: { 119 | name: 'Elizabeth', 120 | children: [{ 121 | name: 'Charles', 122 | children: [{ name: 'William', children: [{ name: 'George' }, { name: 'Charlotte' }] }, { name: 'Harry' }] 123 | }] 124 | }, 125 | directory: { 126 | name: 'directory 1', 127 | subDirectories: [ 128 | { name: 'directory 1.1', subDirectories: [] }, 129 | { 130 | name: 'directory 1.2', 131 | subDirectories: [{ name: 'directory 1.2.1' }, { name: 'directory 1.2.2' }] 132 | } 133 | ] 134 | } 135 | }) 136 | t.assert.equal(value, '{"people":{"name":"Elizabeth","children":[{"name":"Charles","children":[{"name":"William","children":[{"name":"George"},{"name":"Charlotte"}]},{"name":"Harry"}]}]},"directory":{"name":"directory 1","subDirectories":[{"name":"directory 1.1","subDirectories":[]},{"name":"directory 1.2","subDirectories":[{"name":"directory 1.2.1","subDirectories":[]},{"name":"directory 1.2.2","subDirectories":[]}]}]}}') 137 | }) 138 | 139 | test('can stringify recursive references in object types (issue #365)', t => { 140 | t.plan(1) 141 | 142 | const schema = { 143 | type: 'object', 144 | definitions: { 145 | parentCategory: { 146 | type: 'object', 147 | properties: { 148 | parent: { 149 | $ref: '#/definitions/parentCategory' 150 | } 151 | } 152 | } 153 | }, 154 | properties: { 155 | category: { 156 | type: 'object', 157 | properties: { 158 | parent: { 159 | $ref: '#/definitions/parentCategory' 160 | } 161 | } 162 | } 163 | } 164 | } 165 | 166 | const stringify = build(schema) 167 | const data = { 168 | category: { 169 | parent: { 170 | parent: { 171 | parent: { 172 | parent: {} 173 | } 174 | } 175 | } 176 | } 177 | } 178 | const value = stringify(data) 179 | t.assert.equal(value, '{"category":{"parent":{"parent":{"parent":{"parent":{}}}}}}') 180 | }) 181 | 182 | test('can stringify recursive inline $id references (issue #410)', t => { 183 | t.plan(1) 184 | const schema = { 185 | $id: 'Node', 186 | type: 'object', 187 | properties: { 188 | id: { 189 | type: 'string' 190 | }, 191 | nodes: { 192 | type: 'array', 193 | items: { 194 | $ref: 'Node' 195 | } 196 | } 197 | }, 198 | required: [ 199 | 'id', 200 | 'nodes' 201 | ] 202 | } 203 | 204 | const stringify = build(schema) 205 | const data = { 206 | id: '0', 207 | nodes: [ 208 | { 209 | id: '1', 210 | nodes: [{ 211 | id: '2', 212 | nodes: [ 213 | { id: '3', nodes: [] }, 214 | { id: '4', nodes: [] }, 215 | { id: '5', nodes: [] } 216 | ] 217 | }] 218 | }, 219 | { 220 | id: '6', 221 | nodes: [{ 222 | id: '7', 223 | nodes: [ 224 | { id: '8', nodes: [] }, 225 | { id: '9', nodes: [] }, 226 | { id: '10', nodes: [] } 227 | ] 228 | }] 229 | }, 230 | { 231 | id: '11', 232 | nodes: [{ 233 | id: '12', 234 | nodes: [ 235 | { id: '13', nodes: [] }, 236 | { id: '14', nodes: [] }, 237 | { id: '15', nodes: [] } 238 | ] 239 | }] 240 | } 241 | ] 242 | } 243 | const value = stringify(data) 244 | t.assert.equal(value, '{"id":"0","nodes":[{"id":"1","nodes":[{"id":"2","nodes":[{"id":"3","nodes":[]},{"id":"4","nodes":[]},{"id":"5","nodes":[]}]}]},{"id":"6","nodes":[{"id":"7","nodes":[{"id":"8","nodes":[]},{"id":"9","nodes":[]},{"id":"10","nodes":[]}]}]},{"id":"11","nodes":[{"id":"12","nodes":[{"id":"13","nodes":[]},{"id":"14","nodes":[]},{"id":"15","nodes":[]}]}]}]}') 245 | }) 246 | -------------------------------------------------------------------------------- /test/ref.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": { 3 | "def": { 4 | "type": "object", 5 | "properties": { 6 | "str": { 7 | "type": "string" 8 | } 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/regex.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const validator = require('is-my-json-valid') 5 | const build = require('..') 6 | 7 | test('object with RexExp', (t) => { 8 | t.plan(3) 9 | 10 | const schema = { 11 | title: 'object with RegExp', 12 | type: 'object', 13 | properties: { 14 | reg: { 15 | type: 'string' 16 | } 17 | } 18 | } 19 | 20 | const obj = { 21 | reg: /"([^"]|\\")*"/ 22 | } 23 | 24 | const stringify = build(schema) 25 | const validate = validator(schema) 26 | const output = stringify(obj) 27 | 28 | t.assert.doesNotThrow(() => JSON.parse(output)) 29 | 30 | t.assert.equal(obj.reg.source, new RegExp(JSON.parse(output).reg).source) 31 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 32 | }) 33 | -------------------------------------------------------------------------------- /test/required.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('object with required field', (t) => { 7 | t.plan(2) 8 | 9 | const schema = { 10 | title: 'object with required field', 11 | type: 'object', 12 | properties: { 13 | str: { 14 | type: 'string' 15 | }, 16 | num: { 17 | type: 'integer' 18 | } 19 | }, 20 | required: ['str'] 21 | } 22 | const stringify = build(schema) 23 | 24 | t.assert.doesNotThrow(() => { 25 | stringify({ 26 | str: 'string' 27 | }) 28 | }) 29 | 30 | t.assert.throws(() => { 31 | stringify({ 32 | num: 42 33 | }) 34 | }, { message: '"str" is required!' }) 35 | }) 36 | 37 | test('object with required field not in properties schema', (t) => { 38 | t.plan(2) 39 | 40 | const schema = { 41 | title: 'object with required field', 42 | type: 'object', 43 | properties: { 44 | num: { 45 | type: 'integer' 46 | } 47 | }, 48 | required: ['str'] 49 | } 50 | const stringify = build(schema) 51 | 52 | t.assert.throws(() => { 53 | stringify({}) 54 | }, { message: '"str" is required!' }) 55 | 56 | t.assert.throws(() => { 57 | stringify({ 58 | num: 42 59 | }) 60 | }, { message: '"str" is required!' }) 61 | }) 62 | 63 | test('object with required field not in properties schema with additional properties true', (t) => { 64 | t.plan(2) 65 | 66 | const schema = { 67 | title: 'object with required field', 68 | type: 'object', 69 | properties: { 70 | num: { 71 | type: 'integer' 72 | } 73 | }, 74 | additionalProperties: true, 75 | required: ['str'] 76 | } 77 | const stringify = build(schema) 78 | 79 | t.assert.throws(() => { 80 | stringify({}) 81 | }, { message: '"str" is required!' }) 82 | 83 | t.assert.throws(() => { 84 | stringify({ 85 | num: 42 86 | }) 87 | }, { message: '"str" is required!' }) 88 | }) 89 | 90 | test('object with multiple required field not in properties schema', (t) => { 91 | t.plan(3) 92 | 93 | const schema = { 94 | title: 'object with required field', 95 | type: 'object', 96 | properties: { 97 | num: { 98 | type: 'integer' 99 | } 100 | }, 101 | additionalProperties: true, 102 | required: ['num', 'key1', 'key2'] 103 | } 104 | const stringify = build(schema) 105 | 106 | t.assert.throws(() => { 107 | stringify({}) 108 | }, { message: '"key1" is required!' }) 109 | 110 | t.assert.throws(() => { 111 | stringify({ 112 | key1: 42, 113 | key2: 42 114 | }) 115 | }, { message: '"num" is required!' }) 116 | 117 | t.assert.throws(() => { 118 | stringify({ 119 | num: 42, 120 | key1: 'some' 121 | }) 122 | }, { message: '"key2" is required!' }) 123 | }) 124 | 125 | test('object with required bool', (t) => { 126 | t.plan(2) 127 | 128 | const schema = { 129 | title: 'object with required field', 130 | type: 'object', 131 | properties: { 132 | num: { 133 | type: 'integer' 134 | } 135 | }, 136 | additionalProperties: true, 137 | required: ['bool'] 138 | } 139 | const stringify = build(schema) 140 | 141 | t.assert.throws(() => { 142 | stringify({}) 143 | }, { message: '"bool" is required!' }) 144 | 145 | t.assert.doesNotThrow(() => { 146 | stringify({ 147 | bool: false 148 | }) 149 | }) 150 | }) 151 | 152 | test('required nullable', (t) => { 153 | t.plan(1) 154 | 155 | const schema = { 156 | title: 'object with required field', 157 | type: 'object', 158 | properties: { 159 | num: { 160 | type: ['integer'] 161 | } 162 | }, 163 | additionalProperties: true, 164 | required: ['null'] 165 | } 166 | const stringify = build(schema) 167 | 168 | t.assert.doesNotThrow(() => { 169 | stringify({ 170 | null: null 171 | }) 172 | }) 173 | }) 174 | 175 | test('required numbers', (t) => { 176 | t.plan(2) 177 | 178 | const schema = { 179 | title: 'object with required field', 180 | type: 'object', 181 | properties: { 182 | str: { 183 | type: 'string' 184 | }, 185 | num: { 186 | type: 'integer' 187 | } 188 | }, 189 | required: ['num'] 190 | } 191 | const stringify = build(schema) 192 | 193 | t.assert.doesNotThrow(() => { 194 | stringify({ 195 | num: 42 196 | }) 197 | }) 198 | 199 | t.assert.throws(() => { 200 | stringify({ 201 | num: 'aaa' 202 | }) 203 | }, { message: 'The value "aaa" cannot be converted to an integer.' }) 204 | }) 205 | -------------------------------------------------------------------------------- /test/requiresAjv.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('nested ref requires ajv', async t => { 7 | t.test('nested ref requires ajv', async t => { 8 | const schemaA = { 9 | $id: 'urn:schema:a', 10 | definitions: { 11 | foo: { anyOf: [{ type: 'string' }, { type: 'null' }] } 12 | } 13 | } 14 | 15 | const schemaB = { 16 | $id: 'urn:schema:b', 17 | type: 'object', 18 | properties: { 19 | results: { 20 | type: 'object', 21 | properties: { 22 | items: { 23 | type: 'object', 24 | properties: { 25 | bar: { 26 | type: 'array', 27 | items: { $ref: 'urn:schema:a#/definitions/foo' } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | const stringify = build(schemaB, { 37 | schema: { 38 | [schemaA.$id]: schemaA 39 | } 40 | }) 41 | const result = stringify({ 42 | results: { 43 | items: { 44 | bar: ['baz'] 45 | } 46 | } 47 | }) 48 | t.assert.equal(result, '{"results":{"items":{"bar":["baz"]}}}') 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/sanitize.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | const stringify = build({ 7 | title: 'Example Schema', 8 | type: 'object', 9 | properties: { 10 | firstName: { 11 | type: 'string' 12 | }, 13 | lastName: { 14 | type: 'string' 15 | }, 16 | age: { 17 | description: 'Age in years"', 18 | type: 'integer' 19 | }, 20 | [(() => "phra'&& process.exit(1) ||'phra")()]: {}, 21 | now: { 22 | type: 'string' 23 | }, 24 | reg: { 25 | type: 'string', 26 | default: 'a\'&& process.exit(1) ||\'' 27 | }, 28 | obj: { 29 | type: 'object', 30 | properties: { 31 | bool: { 32 | type: 'boolean' 33 | } 34 | } 35 | }, 36 | '"\'w00t': { 37 | type: 'string', 38 | default: '"\'w00t' 39 | }, 40 | arr: { 41 | type: 'array', 42 | items: { 43 | type: 'object', 44 | properties: { 45 | 'phra\' && process.exit(1)//': { 46 | type: 'number' 47 | }, 48 | str: { 49 | type: 'string' 50 | } 51 | } 52 | } 53 | } 54 | }, 55 | required: ['now'], 56 | patternProperties: { 57 | '.*foo$': { 58 | type: 'string' 59 | }, 60 | test: { 61 | type: 'number' 62 | }, 63 | 'phra\'/ && process.exit(1) && /\'': { 64 | type: 'number' 65 | }, 66 | '"\'w00t.*////': { 67 | type: 'number' 68 | } 69 | }, 70 | additionalProperties: { 71 | type: 'string' 72 | } 73 | }) 74 | 75 | const obj = { 76 | firstName: 'Matteo', 77 | lastName: 'Collina', 78 | age: 32, 79 | now: new Date(), 80 | foo: 'hello"', 81 | bar: "world'", 82 | 'fuzz"': 42, 83 | "me'": 42, 84 | numfoo: 42, 85 | test: 42, 86 | strtest: '23', 87 | arr: [{ 'phra\' && process.exit(1)//': 42 }], 88 | obj: { bool: true }, 89 | notmatch: 'valar morghulis', 90 | notmatchobj: { a: true }, 91 | notmatchnum: 42 92 | } 93 | 94 | test('sanitize', t => { 95 | const json = stringify(obj) 96 | t.assert.doesNotThrow(() => JSON.parse(json)) 97 | 98 | const stringify2 = build({ 99 | title: 'Example Schema', 100 | type: 'object', 101 | patternProperties: { 102 | '"\'w00t.*////': { 103 | type: 'number' 104 | } 105 | } 106 | }) 107 | 108 | t.assert.deepStrictEqual(JSON.parse(stringify2({ 109 | '"\'phra////': 42, 110 | asd: 42 111 | })), { 112 | }) 113 | 114 | const stringify3 = build({ 115 | title: 'Example Schema', 116 | type: 'object', 117 | properties: { 118 | "\"phra\\'&&(console.log(42))//||'phra": {} 119 | } 120 | }) 121 | 122 | // this verifies the escaping 123 | JSON.parse(stringify3({ 124 | '"phra\'&&(console.log(42))//||\'phra': 42 125 | })) 126 | 127 | const stringify4 = build({ 128 | title: 'Example Schema', 129 | type: 'object', 130 | properties: { 131 | '"\\\\\\\\\'w00t': { 132 | type: 'string', 133 | default: '"\'w00t' 134 | } 135 | } 136 | }) 137 | 138 | t.assert.deepStrictEqual(JSON.parse(stringify4({})), { 139 | '"\\\\\\\\\'w00t': '"\'w00t' 140 | }) 141 | }) 142 | -------------------------------------------------------------------------------- /test/sanitize2.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('sanitize 2', t => { 7 | const payload = '(throw "pwoned")' 8 | 9 | const stringify = build({ 10 | properties: { 11 | [`*///\\\\\\']);${payload};{/*`]: { 12 | type: 'number' 13 | } 14 | } 15 | }) 16 | 17 | t.assert.doesNotThrow(() => stringify({})) 18 | }) 19 | -------------------------------------------------------------------------------- /test/sanitize3.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('sanitize 3', t => { 7 | t.assert.throws(() => { 8 | build({ 9 | $defs: { 10 | type: 'foooo"bar' 11 | }, 12 | patternProperties: { 13 | x: { $ref: '#/$defs' } 14 | } 15 | }) 16 | }, { message: 'foooo"bar unsupported' }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/sanitize4.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('sanitize 4', t => { 7 | const payload = '(throw "pwoned")' 8 | 9 | const stringify = build({ 10 | required: [`"];${payload}//`] 11 | }) 12 | 13 | t.assert.throws(() => { 14 | stringify({}) 15 | }, { message: '""];(throw "pwoned")//" is required!' }) 16 | }) 17 | -------------------------------------------------------------------------------- /test/sanitize5.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('sanitize 5', t => { 7 | const payload = '(throw "pwoned")' 8 | 9 | t.assert.throws(() => { 10 | build({ 11 | patternProperties: { 12 | '*': { type: `*/${payload}){//` } 13 | } 14 | }) 15 | }, { message: 'schema is invalid: data/patternProperties must match format "regex"' }) 16 | }) 17 | -------------------------------------------------------------------------------- /test/sanitize6.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('sanitize 6', t => { 7 | const payload = '(throw "pwoned")' 8 | 9 | const stringify = build({ 10 | type: 'object', 11 | properties: { 12 | '/*': { type: 'object' }, 13 | x: { 14 | type: 'object', 15 | properties: { 16 | a: { type: 'string', default: `*/}${payload};{//` } 17 | } 18 | } 19 | } 20 | }) 21 | t.assert.doesNotThrow(() => { stringify({}) }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/sanitize7.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('required property containing single quote, contains property', (t) => { 7 | t.plan(1) 8 | 9 | const stringify = build({ 10 | type: 'object', 11 | properties: { 12 | '\'': { type: 'string' } 13 | }, 14 | required: [ 15 | '\'' 16 | ] 17 | }) 18 | 19 | t.assert.throws(() => stringify({}), new Error('"\'" is required!')) 20 | }) 21 | 22 | test('required property containing double quote, contains property', (t) => { 23 | t.plan(1) 24 | 25 | const stringify = build({ 26 | type: 'object', 27 | properties: { 28 | '"': { type: 'string' } 29 | }, 30 | required: [ 31 | '"' 32 | ] 33 | }) 34 | 35 | t.assert.throws(() => stringify({}), new Error('""" is required!')) 36 | }) 37 | 38 | test('required property containing single quote, does not contain property', (t) => { 39 | t.plan(1) 40 | 41 | const stringify = build({ 42 | type: 'object', 43 | properties: { 44 | a: { type: 'string' } 45 | }, 46 | required: [ 47 | '\'' 48 | ] 49 | }) 50 | 51 | t.assert.throws(() => stringify({}), new Error('"\'" is required!')) 52 | }) 53 | 54 | test('required property containing double quote, does not contain property', (t) => { 55 | t.plan(1) 56 | 57 | const stringify = build({ 58 | type: 'object', 59 | properties: { 60 | a: { type: 'string' } 61 | }, 62 | required: [ 63 | '"' 64 | ] 65 | }) 66 | 67 | t.assert.throws(() => stringify({}), new Error('""" is required!')) 68 | }) 69 | -------------------------------------------------------------------------------- /test/side-effect.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const clone = require('rfdc/default') 5 | const build = require('..') 6 | 7 | test('oneOf with $ref should not change the input schema', t => { 8 | t.plan(2) 9 | 10 | const referenceSchema = { 11 | $id: 'externalId', 12 | type: 'object', 13 | properties: { 14 | name: { type: 'string' } 15 | } 16 | } 17 | 18 | const schema = { 19 | $id: 'mainSchema', 20 | type: 'object', 21 | properties: { 22 | people: { 23 | oneOf: [{ $ref: 'externalId' }] 24 | } 25 | } 26 | } 27 | const clonedSchema = clone(schema) 28 | const stringify = build(schema, { 29 | schema: { 30 | [referenceSchema.$id]: referenceSchema 31 | } 32 | }) 33 | 34 | const value = stringify({ people: { name: 'hello', foo: 'bar' } }) 35 | t.assert.equal(value, '{"people":{"name":"hello"}}') 36 | t.assert.deepStrictEqual(schema, clonedSchema) 37 | }) 38 | 39 | test('oneOf and anyOf with $ref should not change the input schema', t => { 40 | t.plan(3) 41 | 42 | const referenceSchema = { 43 | $id: 'externalSchema', 44 | type: 'object', 45 | properties: { 46 | name: { type: 'string' } 47 | } 48 | } 49 | 50 | const schema = { 51 | $id: 'rootSchema', 52 | type: 'object', 53 | properties: { 54 | people: { 55 | oneOf: [{ $ref: 'externalSchema' }] 56 | }, 57 | love: { 58 | anyOf: [ 59 | { $ref: '#/definitions/foo' }, 60 | { type: 'boolean' } 61 | ] 62 | } 63 | }, 64 | definitions: { 65 | foo: { type: 'string' } 66 | } 67 | } 68 | const clonedSchema = clone(schema) 69 | const stringify = build(schema, { 70 | schema: { 71 | [referenceSchema.$id]: referenceSchema 72 | } 73 | }) 74 | 75 | const valueAny1 = stringify({ people: { name: 'hello', foo: 'bar' }, love: 'music' }) 76 | const valueAny2 = stringify({ people: { name: 'hello', foo: 'bar' }, love: true }) 77 | 78 | t.assert.equal(valueAny1, '{"people":{"name":"hello"},"love":"music"}') 79 | t.assert.equal(valueAny2, '{"people":{"name":"hello"},"love":true}') 80 | t.assert.deepStrictEqual(schema, clonedSchema) 81 | }) 82 | 83 | test('multiple $ref tree', t => { 84 | t.plan(2) 85 | 86 | const referenceDeepSchema = { 87 | $id: 'deepId', 88 | type: 'number' 89 | } 90 | 91 | const referenceSchema = { 92 | $id: 'externalId', 93 | type: 'object', 94 | properties: { 95 | name: { $ref: '#/definitions/foo' }, 96 | age: { $ref: 'deepId' } 97 | }, 98 | definitions: { 99 | foo: { type: 'string' } 100 | } 101 | } 102 | 103 | const schema = { 104 | $id: 'mainSchema', 105 | type: 'object', 106 | properties: { 107 | people: { 108 | oneOf: [{ $ref: 'externalId' }] 109 | } 110 | } 111 | } 112 | const clonedSchema = clone(schema) 113 | const stringify = build(schema, { 114 | schema: { 115 | [referenceDeepSchema.$id]: referenceDeepSchema, 116 | [referenceSchema.$id]: referenceSchema 117 | } 118 | }) 119 | 120 | const value = stringify({ people: { name: 'hello', foo: 'bar', age: 42 } }) 121 | t.assert.equal(value, '{"people":{"name":"hello","age":42}}') 122 | t.assert.deepStrictEqual(schema, clonedSchema) 123 | }) 124 | 125 | test('must not mutate items $ref', t => { 126 | t.plan(2) 127 | 128 | const referenceSchema = { 129 | $id: 'ShowSchema', 130 | $schema: 'http://json-schema.org/draft-07/schema#', 131 | type: 'object', 132 | properties: { 133 | name: { 134 | type: 'string' 135 | } 136 | } 137 | } 138 | 139 | const schema = { 140 | $id: 'ListSchema', 141 | $schema: 'http://json-schema.org/draft-07/schema#', 142 | type: 'array', 143 | items: { 144 | $ref: 'ShowSchema#' 145 | } 146 | } 147 | const clonedSchema = clone(schema) 148 | const stringify = build(schema, { 149 | schema: { 150 | [referenceSchema.$id]: referenceSchema 151 | } 152 | }) 153 | 154 | const value = stringify([{ name: 'foo' }]) 155 | t.assert.equal(value, '[{"name":"foo"}]') 156 | t.assert.deepStrictEqual(schema, clonedSchema) 157 | }) 158 | 159 | test('must not mutate items referred by $ref', t => { 160 | t.plan(2) 161 | 162 | const firstSchema = { 163 | $id: 'example1', 164 | type: 'object', 165 | properties: { 166 | name: { 167 | type: 'string' 168 | } 169 | } 170 | } 171 | 172 | const reusedSchema = { 173 | $id: 'example2', 174 | type: 'object', 175 | properties: { 176 | name: { 177 | oneOf: [ 178 | { 179 | $ref: 'example1' 180 | } 181 | ] 182 | } 183 | } 184 | } 185 | 186 | const clonedSchema = clone(firstSchema) 187 | const stringify = build(reusedSchema, { 188 | schema: { 189 | [firstSchema.$id]: firstSchema 190 | } 191 | }) 192 | 193 | const value = stringify({ name: { name: 'foo' } }) 194 | t.assert.equal(value, '{"name":{"name":"foo"}}') 195 | t.assert.deepStrictEqual(firstSchema, clonedSchema) 196 | }) 197 | -------------------------------------------------------------------------------- /test/standalone-mode.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test, after } = require('node:test') 4 | const fjs = require('..') 5 | const fs = require('fs') 6 | const path = require('path') 7 | 8 | function build (opts, schema) { 9 | return fjs(schema || { 10 | title: 'default string', 11 | type: 'object', 12 | properties: { 13 | firstName: { 14 | type: 'string' 15 | } 16 | }, 17 | required: ['firstName'] 18 | }, opts) 19 | } 20 | 21 | const tmpDir = 'test/fixtures' 22 | 23 | test('activate standalone mode', async (t) => { 24 | t.plan(3) 25 | 26 | after(async () => { 27 | await fs.promises.rm(destination, { force: true }) 28 | }) 29 | 30 | const code = build({ mode: 'standalone' }) 31 | t.assert.ok(typeof code === 'string') 32 | t.assert.equal(code.indexOf('ajv'), -1) 33 | 34 | const destination = path.resolve(tmpDir, 'standalone.js') 35 | 36 | await fs.promises.writeFile(destination, code) 37 | const standalone = require(destination) 38 | t.assert.equal(standalone({ firstName: 'Foo', surname: 'bar' }), JSON.stringify({ firstName: 'Foo' }), 'surname evicted') 39 | }) 40 | 41 | test('test ajv schema', async (t) => { 42 | t.plan(3) 43 | 44 | after(async () => { 45 | await fs.promises.rm(destination, { force: true }) 46 | }) 47 | 48 | const code = build({ mode: 'standalone' }, { 49 | type: 'object', 50 | properties: { 51 | }, 52 | if: { 53 | type: 'object', 54 | properties: { 55 | kind: { type: 'string', enum: ['foobar'] } 56 | } 57 | }, 58 | then: { 59 | type: 'object', 60 | properties: { 61 | kind: { type: 'string', enum: ['foobar'] }, 62 | foo: { type: 'string' }, 63 | bar: { type: 'number' }, 64 | list: { 65 | type: 'array', 66 | items: { 67 | type: 'object', 68 | properties: { 69 | name: { type: 'string' }, 70 | value: { type: 'string' } 71 | } 72 | } 73 | } 74 | } 75 | }, 76 | else: { 77 | type: 'object', 78 | properties: { 79 | kind: { type: 'string', enum: ['greeting'] }, 80 | hi: { type: 'string' }, 81 | hello: { type: 'number' }, 82 | list: { 83 | type: 'array', 84 | items: { 85 | type: 'object', 86 | properties: { 87 | name: { type: 'string' }, 88 | value: { type: 'string' } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | }) 95 | t.assert.ok(typeof code === 'string') 96 | t.assert.equal(code.indexOf('ajv') > 0, true) 97 | 98 | const destination = path.resolve(tmpDir, 'standalone2.js') 99 | 100 | await fs.promises.writeFile(destination, code) 101 | const standalone = require(destination) 102 | t.assert.equal(standalone({ 103 | kind: 'foobar', 104 | foo: 'FOO', 105 | list: [{ 106 | name: 'name', 107 | value: 'foo' 108 | }], 109 | bar: 42, 110 | hi: 'HI', 111 | hello: 45, 112 | a: 'A', 113 | b: 35 114 | }), JSON.stringify({ 115 | kind: 'foobar', 116 | foo: 'FOO', 117 | bar: 42, 118 | list: [{ 119 | name: 'name', 120 | value: 'foo' 121 | }] 122 | })) 123 | }) 124 | 125 | test('no need to keep external schemas once compiled', async (t) => { 126 | t.plan(1) 127 | 128 | after(async () => { 129 | await fs.promises.rm(destination, { force: true }) 130 | }) 131 | 132 | const externalSchema = { 133 | first: { 134 | definitions: { 135 | id1: { 136 | type: 'object', 137 | properties: { 138 | id1: { 139 | type: 'integer' 140 | } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | const code = fjs({ 147 | $ref: 'first#/definitions/id1' 148 | }, { 149 | mode: 'standalone', 150 | schema: externalSchema 151 | }) 152 | 153 | const destination = path.resolve(tmpDir, 'standalone3.js') 154 | 155 | await fs.promises.writeFile(destination, code) 156 | const standalone = require(destination) 157 | 158 | t.assert.equal(standalone({ id1: 5 }), JSON.stringify({ id1: 5 }), 'serialization works with external schemas') 159 | }) 160 | 161 | test('no need to keep external schemas once compiled - with oneOf validator', async (t) => { 162 | t.plan(2) 163 | 164 | after(async () => { 165 | await fs.promises.rm(destination, { force: true }) 166 | }) 167 | 168 | const externalSchema = { 169 | ext: { 170 | definitions: { 171 | oBaz: { 172 | type: 'object', 173 | properties: { 174 | baz: { type: 'number' } 175 | }, 176 | required: ['baz'] 177 | }, 178 | oBar: { 179 | type: 'object', 180 | properties: { 181 | bar: { type: 'string' } 182 | }, 183 | required: ['bar'] 184 | }, 185 | other: { 186 | type: 'string', 187 | const: 'other' 188 | } 189 | } 190 | } 191 | } 192 | 193 | const schema = { 194 | title: 'object with oneOf property value containing refs to external schema', 195 | type: 'object', 196 | properties: { 197 | oneOfSchema: { 198 | oneOf: [ 199 | { $ref: 'ext#/definitions/oBaz' }, 200 | { $ref: 'ext#/definitions/oBar' } 201 | ] 202 | } 203 | }, 204 | required: ['oneOfSchema'] 205 | } 206 | 207 | const code = fjs(schema, { 208 | mode: 'standalone', 209 | schema: externalSchema 210 | }) 211 | 212 | const destination = path.resolve(tmpDir, 'standalone-oneOf-ref.js') 213 | 214 | await fs.promises.writeFile(destination, code) 215 | const stringify = require(destination) 216 | 217 | t.assert.equal(stringify({ oneOfSchema: { baz: 5 } }), '{"oneOfSchema":{"baz":5}}') 218 | t.assert.equal(stringify({ oneOfSchema: { bar: 'foo' } }), '{"oneOfSchema":{"bar":"foo"}}') 219 | }) 220 | -------------------------------------------------------------------------------- /test/string.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | 5 | const build = require('..') 6 | 7 | test('serialize short string', (t) => { 8 | t.plan(2) 9 | 10 | const schema = { 11 | type: 'string' 12 | } 13 | 14 | const input = 'abcd' 15 | const stringify = build(schema) 16 | const output = stringify(input) 17 | 18 | t.assert.equal(output, '"abcd"') 19 | t.assert.equal(JSON.parse(output), input) 20 | }) 21 | 22 | test('serialize short string', (t) => { 23 | t.plan(2) 24 | 25 | const schema = { 26 | type: 'string' 27 | } 28 | 29 | const input = '\x00' 30 | const stringify = build(schema) 31 | const output = stringify(input) 32 | 33 | t.assert.equal(output, '"\\u0000"') 34 | t.assert.equal(JSON.parse(output), input) 35 | }) 36 | 37 | test('serialize long string', (t) => { 38 | t.plan(2) 39 | 40 | const schema = { 41 | type: 'string' 42 | } 43 | 44 | const input = new Array(2e4).fill('\x00').join('') 45 | const stringify = build(schema) 46 | const output = stringify(input) 47 | 48 | t.assert.equal(output, `"${new Array(2e4).fill('\\u0000').join('')}"`) 49 | t.assert.equal(JSON.parse(output), input) 50 | }) 51 | 52 | test('unsafe string', (t) => { 53 | t.plan(2) 54 | 55 | const schema = { 56 | type: 'string', 57 | format: 'unsafe' 58 | } 59 | 60 | const input = 'abcd' 61 | const stringify = build(schema) 62 | const output = stringify(input) 63 | 64 | t.assert.equal(output, `"${input}"`) 65 | t.assert.equal(JSON.parse(output), input) 66 | }) 67 | 68 | test('unsafe unescaped string', (t) => { 69 | t.plan(2) 70 | 71 | const schema = { 72 | type: 'string', 73 | format: 'unsafe' 74 | } 75 | 76 | const input = 'abcd "abcd"' 77 | const stringify = build(schema) 78 | const output = stringify(input) 79 | 80 | t.assert.equal(output, `"${input}"`) 81 | t.assert.throws(function () { 82 | JSON.parse(output) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /test/surrogate.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const validator = require('is-my-json-valid') 5 | const build = require('..') 6 | 7 | test('render a string with surrogate pairs as JSON:test 1', (t) => { 8 | t.plan(2) 9 | 10 | const schema = { 11 | title: 'surrogate', 12 | type: 'string' 13 | } 14 | 15 | const validate = validator(schema) 16 | const stringify = build(schema) 17 | const output = stringify('𝌆') 18 | 19 | t.assert.equal(output, '"𝌆"') 20 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 21 | }) 22 | 23 | test('render a string with surrogate pairs as JSON: test 2', (t) => { 24 | t.plan(2) 25 | 26 | const schema = { 27 | title: 'long', 28 | type: 'string' 29 | } 30 | 31 | const validate = validator(schema) 32 | const stringify = build(schema) 33 | const output = stringify('\uD834\uDF06') 34 | 35 | t.assert.equal(output, '"𝌆"') 36 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 37 | }) 38 | 39 | test('render a string with Unpaired surrogate code as JSON', (t) => { 40 | t.plan(2) 41 | 42 | const schema = { 43 | title: 'surrogate', 44 | type: 'string' 45 | } 46 | 47 | const validate = validator(schema) 48 | const stringify = build(schema) 49 | const output = stringify('\uDF06\uD834') 50 | t.assert.equal(output, JSON.stringify('\uDF06\uD834')) 51 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 52 | }) 53 | 54 | test('render a string with lone surrogate code as JSON', (t) => { 55 | t.plan(2) 56 | 57 | const schema = { 58 | title: 'surrogate', 59 | type: 'string' 60 | } 61 | 62 | const validate = validator(schema) 63 | const stringify = build(schema) 64 | const output = stringify('\uDEAD') 65 | t.assert.equal(output, JSON.stringify('\uDEAD')) 66 | t.assert.ok(validate(JSON.parse(output)), 'valid schema') 67 | }) 68 | -------------------------------------------------------------------------------- /test/toJSON.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('use toJSON method on object types', (t) => { 7 | t.plan(1) 8 | 9 | const stringify = build({ 10 | title: 'simple object', 11 | type: 'object', 12 | properties: { 13 | productName: { 14 | type: 'string' 15 | } 16 | } 17 | }) 18 | const object = { 19 | product: { name: 'cola' }, 20 | toJSON: function () { 21 | return { productName: this.product.name } 22 | } 23 | } 24 | 25 | t.assert.equal('{"productName":"cola"}', stringify(object)) 26 | }) 27 | 28 | test('use toJSON method on nested object types', (t) => { 29 | t.plan(1) 30 | 31 | const stringify = build({ 32 | title: 'simple array', 33 | type: 'array', 34 | items: { 35 | type: 'object', 36 | properties: { 37 | productName: { 38 | type: 'string' 39 | } 40 | } 41 | } 42 | }) 43 | const array = [ 44 | { 45 | product: { name: 'cola' }, 46 | toJSON: function () { 47 | return { productName: this.product.name } 48 | } 49 | }, 50 | { 51 | product: { name: 'sprite' }, 52 | toJSON: function () { 53 | return { productName: this.product.name } 54 | } 55 | } 56 | ] 57 | 58 | t.assert.equal('[{"productName":"cola"},{"productName":"sprite"}]', stringify(array)) 59 | }) 60 | 61 | test('not use toJSON if does not exist', (t) => { 62 | t.plan(1) 63 | 64 | const stringify = build({ 65 | title: 'simple object', 66 | type: 'object', 67 | properties: { 68 | product: { 69 | type: 'object', 70 | properties: { 71 | name: { 72 | type: 'string' 73 | } 74 | } 75 | } 76 | } 77 | }) 78 | const object = { 79 | product: { name: 'cola' } 80 | } 81 | 82 | t.assert.equal('{"product":{"name":"cola"}}', stringify(object)) 83 | }) 84 | 85 | test('not fail on null object declared nullable', (t) => { 86 | t.plan(1) 87 | 88 | const stringify = build({ 89 | title: 'simple object', 90 | type: 'object', 91 | nullable: true, 92 | properties: { 93 | product: { 94 | type: 'object', 95 | properties: { 96 | name: { 97 | type: 'string' 98 | } 99 | } 100 | } 101 | } 102 | }) 103 | t.assert.equal('null', stringify(null)) 104 | }) 105 | 106 | test('not fail on null sub-object declared nullable', (t) => { 107 | t.plan(1) 108 | 109 | const stringify = build({ 110 | title: 'simple object', 111 | type: 'object', 112 | properties: { 113 | product: { 114 | nullable: true, 115 | type: 'object', 116 | properties: { 117 | name: { 118 | type: 'string' 119 | } 120 | } 121 | } 122 | } 123 | }) 124 | const object = { 125 | product: null 126 | } 127 | t.assert.equal('{"product":null}', stringify(object)) 128 | }) 129 | 130 | test('on non nullable null sub-object it should coerce to {}', (t) => { 131 | t.plan(1) 132 | 133 | const stringify = build({ 134 | title: 'simple object', 135 | type: 'object', 136 | properties: { 137 | product: { 138 | nullable: false, 139 | type: 'object', 140 | properties: { 141 | name: { 142 | type: 'string' 143 | } 144 | } 145 | } 146 | } 147 | }) 148 | const object = { 149 | product: null 150 | } 151 | 152 | const result = stringify(object) 153 | t.assert.equal(result, JSON.stringify({ product: {} })) 154 | }) 155 | 156 | test('on non nullable null object it should coerce to {}', (t) => { 157 | t.plan(1) 158 | 159 | const stringify = build({ 160 | title: 'simple object', 161 | nullable: false, 162 | type: 'object', 163 | properties: { 164 | product: { 165 | nullable: false, 166 | type: 'object', 167 | properties: { 168 | name: { 169 | type: 'string' 170 | } 171 | } 172 | } 173 | } 174 | }) 175 | 176 | const result = stringify(null) 177 | t.assert.equal(result, '{}') 178 | }) 179 | 180 | test('on non-nullable null object it should skip rendering, skipping required fields checks', (t) => { 181 | t.plan(1) 182 | 183 | const stringify = build({ 184 | title: 'simple object', 185 | nullable: false, 186 | type: 'object', 187 | properties: { 188 | product: { 189 | nullable: false, 190 | type: 'object', 191 | properties: { 192 | name: { 193 | type: 'string' 194 | } 195 | } 196 | } 197 | }, 198 | required: ['product'] 199 | }) 200 | 201 | const result = stringify(null) 202 | t.assert.equal(result, '{}') 203 | }) 204 | -------------------------------------------------------------------------------- /test/typebox.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('nested object in pattern properties for typebox', (t) => { 7 | const { Type } = require('@sinclair/typebox') 8 | 9 | t.plan(1) 10 | 11 | const nestedSchema = Type.Object({ 12 | nestedKey1: Type.String() 13 | }) 14 | 15 | const RootSchema = Type.Object({ 16 | key1: Type.Record(Type.String(), nestedSchema), 17 | key2: Type.Record(Type.String(), nestedSchema) 18 | }) 19 | 20 | const schema = RootSchema 21 | const stringify = build(schema) 22 | 23 | const value = stringify({ 24 | key1: { 25 | nestedKey: { 26 | nestedKey1: 'value1' 27 | } 28 | }, 29 | key2: { 30 | nestedKey: { 31 | nestedKey1: 'value2' 32 | } 33 | } 34 | }) 35 | t.assert.equal(value, '{"key1":{"nestedKey":{"nestedKey1":"value1"}},"key2":{"nestedKey":{"nestedKey1":"value2"}}}') 36 | }) 37 | -------------------------------------------------------------------------------- /test/unknownFormats.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const build = require('..') 5 | 6 | test('object with custom format field', (t) => { 7 | t.plan(1) 8 | 9 | const schema = { 10 | title: 'object with custom format field', 11 | type: 'object', 12 | properties: { 13 | str: { 14 | type: 'string', 15 | format: 'test-format' 16 | } 17 | } 18 | } 19 | 20 | const stringify = build(schema) 21 | 22 | t.assert.doesNotThrow(() => { 23 | stringify({ 24 | str: 'string' 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/webpack.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const webpack = require('webpack') 5 | const path = require('path') 6 | 7 | test('the library should work with webpack', async (t) => { 8 | t.plan(1) 9 | const targetdir = path.resolve(__dirname, '..', '.cache') 10 | const targetname = path.join(targetdir, 'webpacktest.js') 11 | const wopts = { 12 | entry: path.resolve(__dirname, '..', 'index.js'), 13 | mode: 'production', 14 | target: 'node', 15 | output: { 16 | path: targetdir, 17 | filename: 'webpacktest.js', 18 | library: { 19 | name: 'fastJsonStringify', 20 | type: 'umd' 21 | } 22 | } 23 | } 24 | await new Promise((resolve, reject) => { 25 | webpack(wopts, (err, stats) => { 26 | if (err) { reject(err) } else { resolve(stats) }; 27 | }) 28 | }) 29 | const build = require(targetname) 30 | const stringify = build({ 31 | title: 'webpack should not rename code to be executed', 32 | type: 'object', 33 | properties: { 34 | foo: { 35 | type: 'string' 36 | }, 37 | bar: { 38 | type: 'boolean' 39 | } 40 | }, 41 | patternProperties: { 42 | foo: { 43 | type: 'number' 44 | } 45 | } 46 | }) 47 | 48 | const obj = { foo: '42', bar: true } 49 | t.assert.equal(stringify(obj), '{"foo":"42","bar":true}') 50 | }) 51 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import Ajv, { Options as AjvOptions } from 'ajv' 2 | 3 | type Build = typeof build 4 | 5 | declare namespace build { 6 | interface BaseSchema { 7 | /** 8 | * Schema id 9 | */ 10 | $id?: string 11 | /** 12 | * Schema title 13 | */ 14 | title?: string; 15 | /** 16 | * Schema description 17 | */ 18 | description?: string; 19 | /** 20 | * A comment to be added to the schema 21 | */ 22 | $comment?: string; 23 | /** 24 | * Default value to be assigned when no value is given in the document 25 | */ 26 | default?: any; 27 | /** 28 | * A list of example values that match this schema 29 | */ 30 | examples?: any[]; 31 | /** 32 | * Additional schema definition to reference from within the schema 33 | */ 34 | definitions?: Record 35 | /** 36 | * A set of schemas of which at least one must match 37 | */ 38 | anyOf?: Partial[]; 39 | /** 40 | * A set of schemas which must all match 41 | */ 42 | allOf?: Partial[]; 43 | /** 44 | * A conditional schema to check, controls schemas defined in `then` and `else` 45 | */ 46 | if?: Partial; 47 | /** 48 | * A schema to apply if the conditional schema from `if` passes 49 | */ 50 | then?: Partial; 51 | /** 52 | * A schema to apply if the conditional schema from `if` fails 53 | */ 54 | else?: Partial; 55 | /** 56 | * Open API 3.0 spec states that any value that can be null must be declared `nullable` 57 | * @default false 58 | */ 59 | nullable?: boolean; 60 | } 61 | 62 | export interface RefSchema { 63 | /** 64 | * A json-pointer to a schema to use as a reference 65 | */ 66 | $ref: string; 67 | } 68 | 69 | export interface AnySchema extends BaseSchema { 70 | } 71 | 72 | export interface StringSchema extends BaseSchema { 73 | type: 'string'; 74 | format?: string; 75 | } 76 | 77 | export interface IntegerSchema extends BaseSchema { 78 | type: 'integer'; 79 | } 80 | 81 | export interface NumberSchema extends BaseSchema { 82 | type: 'number'; 83 | } 84 | 85 | export interface NullSchema extends BaseSchema { 86 | type: 'null'; 87 | } 88 | 89 | export interface BooleanSchema extends BaseSchema { 90 | type: 'boolean'; 91 | } 92 | 93 | export interface ArraySchema extends BaseSchema { 94 | type: 'array'; 95 | /** 96 | * The schema for the items in the array 97 | */ 98 | items: Schema | {} 99 | } 100 | 101 | export interface TupleSchema extends BaseSchema { 102 | type: 'array'; 103 | /** 104 | * The schemas for the items in the tuple 105 | */ 106 | items: Schema[]; 107 | } 108 | 109 | type ObjectProperties = Record> & { 110 | anyOf?: ObjectProperties[]; 111 | allOf?: ObjectProperties[]; 112 | if?: ObjectProperties; 113 | then?: ObjectProperties; 114 | else?: ObjectProperties; 115 | } 116 | 117 | export interface ObjectSchema extends BaseSchema { 118 | type: 'object'; 119 | /** 120 | * Describe the properties of the object 121 | */ 122 | properties?: ObjectProperties; 123 | /** 124 | * The required properties of the object 125 | */ 126 | required?: string[]; 127 | /** 128 | * Describe properties that have keys following a given pattern 129 | */ 130 | patternProperties?: ObjectProperties; 131 | /** 132 | * Specifies whether additional properties on the object are allowed, and optionally what schema they should 133 | * adhere to 134 | * @default false 135 | */ 136 | additionalProperties?: Schema | boolean; 137 | } 138 | 139 | export type Schema = 140 | | RefSchema 141 | | StringSchema 142 | | IntegerSchema 143 | | NumberSchema 144 | | NullSchema 145 | | BooleanSchema 146 | | ArraySchema 147 | | TupleSchema 148 | | ObjectSchema 149 | 150 | export interface Options { 151 | /** 152 | * Optionally add an external definition to reference from your schema 153 | */ 154 | schema?: Record 155 | /** 156 | * Configure Ajv, which is used to evaluate conditional schemas and combined (anyOf) schemas 157 | */ 158 | ajv?: AjvOptions 159 | /** 160 | * Optionally configure how the integer will be rounded 161 | * 162 | * @default 'trunc' 163 | */ 164 | rounding?: 'ceil' | 'floor' | 'round' | 'trunc' 165 | /** 166 | * @deprecated 167 | * Enable debug mode. Please use `mode: "debug"` instead 168 | */ 169 | debugMode?: boolean 170 | /** 171 | * Running mode of fast-json-stringify 172 | */ 173 | mode?: 'debug' | 'standalone' 174 | 175 | /** 176 | * Large arrays are defined as arrays containing, by default, `20000` 177 | * elements or more. That value can be adjusted via the option parameter 178 | * `largeArraySize`. 179 | * 180 | * @default 20000 181 | */ 182 | largeArraySize?: number | string | BigInt 183 | 184 | /** 185 | * Specify the function on how large Arrays should be stringified. 186 | * 187 | * @default 'default' 188 | */ 189 | largeArrayMechanism?: 'default' | 'json-stringify' 190 | } 191 | 192 | export const validLargeArrayMechanisms: string[] 193 | export function restore (value: (doc: TDoc) => string): ReturnType 194 | 195 | export const build: Build 196 | export { build as default } 197 | } 198 | 199 | interface DebugOption extends build.Options { 200 | mode: 'debug' 201 | } 202 | 203 | interface DeprecateDebugOption extends build.Options { 204 | debugMode: true 205 | } 206 | 207 | interface StandaloneOption extends build.Options { 208 | mode: 'standalone' 209 | } 210 | 211 | type StringCoercible = string | Date | RegExp 212 | type IntegerCoercible = number | BigInt 213 | 214 | /** 215 | * Build a stringify function using a schema of the documents that should be stringified 216 | * @param schema The schema used to stringify values 217 | * @param options The options to use (optional) 218 | */ 219 | declare function build (schema: build.AnySchema, options: DebugOption): { code: string, ajv: Ajv } 220 | declare function build (schema: build.AnySchema, options: DeprecateDebugOption): { code: string, ajv: Ajv } 221 | declare function build (schema: build.AnySchema, options: StandaloneOption): string 222 | declare function build (schema: build.AnySchema, options?: build.Options): (doc: TDoc) => any 223 | declare function build (schema: build.StringSchema, options?: build.Options): (doc: TDoc) => string 224 | declare function build (schema: build.IntegerSchema | build.NumberSchema, options?: build.Options): (doc: TDoc) => string 225 | declare function build (schema: build.NullSchema, options?: build.Options): (doc: TDoc) => 'null' 226 | declare function build (schema: build.BooleanSchema, options?: build.Options): (doc: TDoc) => string 227 | declare function build (schema: build.ArraySchema | build.TupleSchema, options?: build.Options): (doc: TDoc) => string 228 | declare function build (schema: build.ObjectSchema, options?: build.Options): (doc: TDoc) => string 229 | declare function build (schema: build.Schema, options?: build.Options): (doc: TDoc) => string 230 | 231 | export = build 232 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Test using this disabled, see https://github.com/fastify/fast-json-stringify/pull/683 2 | import Ajv from 'ajv' 3 | import build, { restore, Schema, validLargeArrayMechanisms } from '..' 4 | import { expectError, expectType } from 'tsd' 5 | 6 | // Number schemas 7 | build({ 8 | type: 'number' 9 | })(25) 10 | build({ 11 | type: 'integer' 12 | })(-5) 13 | build({ 14 | type: 'integer' 15 | })(5n) 16 | 17 | build({ 18 | type: 'number' 19 | }, { rounding: 'ceil' }) 20 | build({ 21 | type: 'number' 22 | }, { rounding: 'floor' }) 23 | build({ 24 | type: 'number' 25 | }, { rounding: 'round' }) 26 | build({ 27 | type: 'number' 28 | }, { rounding: 'trunc' }) 29 | expectError(build({ 30 | type: 'number' 31 | }, { rounding: 'invalid' })) 32 | 33 | // String schema 34 | build({ 35 | type: 'string' 36 | })('foobar') 37 | 38 | // Boolean schema 39 | build({ 40 | type: 'boolean' 41 | })(true) 42 | 43 | // Null schema 44 | build({ 45 | type: 'null' 46 | })(null) 47 | 48 | // Array schemas 49 | build({ 50 | type: 'array', 51 | items: { type: 'number' } 52 | })([25]) 53 | build({ 54 | type: 'array', 55 | items: [{ type: 'string' }, { type: 'integer' }] 56 | })(['hello', 42]) 57 | 58 | // Object schemas 59 | build({ 60 | type: 'object' 61 | })({}) 62 | build({ 63 | type: 'object', 64 | properties: { 65 | foo: { type: 'string' }, 66 | bar: { type: 'integer' } 67 | }, 68 | required: ['foo'], 69 | patternProperties: { 70 | 'baz*': { type: 'null' } 71 | }, 72 | additionalProperties: { 73 | type: 'boolean' 74 | } 75 | })({ foo: 'bar' }) 76 | build({ 77 | type: 'object', 78 | properties: { 79 | foo: { type: 'string' }, 80 | bar: { type: 'integer' } 81 | }, 82 | required: ['foo'], 83 | patternProperties: { 84 | 'baz*': { type: 'null' } 85 | }, 86 | additionalProperties: { 87 | type: 'boolean' 88 | } 89 | }, { rounding: 'floor' })({ foo: 'bar' }) 90 | 91 | // Reference schemas 92 | build({ 93 | title: 'Example Schema', 94 | definitions: { 95 | num: { 96 | type: 'object', 97 | properties: { 98 | int: { 99 | type: 'integer' 100 | } 101 | } 102 | }, 103 | str: { 104 | type: 'string' 105 | }, 106 | def: { 107 | type: 'null' 108 | } 109 | }, 110 | type: 'object', 111 | properties: { 112 | nickname: { 113 | $ref: '#/definitions/str' 114 | } 115 | }, 116 | patternProperties: { 117 | num: { 118 | $ref: '#/definitions/num' 119 | } 120 | }, 121 | additionalProperties: { 122 | $ref: '#/definitions/def' 123 | } 124 | })({ nickname: '', num: { int: 5 }, other: null }) 125 | 126 | // Conditional/Combined schemas 127 | build({ 128 | title: 'Conditional/Combined Schema', 129 | type: 'object', 130 | properties: { 131 | something: { 132 | anyOf: [ 133 | { type: 'string' }, 134 | { type: 'boolean' } 135 | ] 136 | } 137 | }, 138 | if: { 139 | properties: { 140 | something: { type: 'string' } 141 | } 142 | }, 143 | then: { 144 | properties: { 145 | somethingElse: { type: 'number' } 146 | } 147 | }, 148 | else: { 149 | properties: { 150 | somethingElse: { type: 'null' } 151 | } 152 | } 153 | })({ something: 'a string', somethingElse: 42 }) 154 | 155 | // String schema with format 156 | 157 | build({ 158 | type: 'string', 159 | format: 'date-time' 160 | })(new Date()) 161 | 162 | /* 163 | This overload doesn't work yet - 164 | TypeScript chooses the generic for the schema 165 | before it chooses the overload for the options 166 | parameter. 167 | let str: string, ajv: Ajv 168 | str = build({ 169 | type: 'number' 170 | }, { debugMode: true }).code 171 | ajv = build({ 172 | type: 'number' 173 | }, { debugMode: true }).ajv 174 | str = build({ 175 | type: 'number' 176 | }, { mode: 'debug' }).code 177 | ajv = build({ 178 | type: 'number' 179 | }, { mode: 'debug' }).ajv 180 | str = build({ 181 | type: 'number' 182 | }, { mode: 'standalone' }) 183 | */ 184 | 185 | const debugCompiled = build({ 186 | title: 'default string', 187 | type: 'object', 188 | properties: { 189 | firstName: { 190 | type: 'string' 191 | } 192 | } 193 | }, { mode: 'debug' }) 194 | expectType>(build.restore(debugCompiled)) 195 | expectType>(restore(debugCompiled)) 196 | 197 | expectType(build.validLargeArrayMechanisms) 198 | expectType(validLargeArrayMechanisms) 199 | 200 | /** 201 | * Schema inference 202 | */ 203 | 204 | // With inference 205 | interface InferenceSchema { 206 | id: string; 207 | a?: number; 208 | } 209 | 210 | const stringify3 = build({ 211 | type: 'object', 212 | properties: { a: { type: 'string' } }, 213 | }) 214 | stringify3({ id: '123' }) 215 | stringify3({ a: 123, id: '123' }) 216 | expectError(stringify3({ anotherOne: 'bar' })) 217 | expectError(stringify3({ a: 'bar' })) 218 | 219 | // Without inference 220 | const stringify4 = build({ 221 | type: 'object', 222 | properties: { a: { type: 'string' } }, 223 | }) 224 | stringify4({ id: '123' }) 225 | stringify4({ a: 123, id: '123' }) 226 | stringify4({ anotherOne: 'bar' }) 227 | stringify4({ a: 'bar' }) 228 | 229 | // Without inference - string type 230 | const stringify5 = build({ 231 | type: 'string', 232 | }) 233 | stringify5('foo') 234 | expectError(stringify5({ id: '123' })) 235 | 236 | // Without inference - null type 237 | const stringify6 = build({ 238 | type: 'null', 239 | }) 240 | stringify6(null) 241 | expectError(stringify6('a string')) 242 | 243 | // Without inference - boolean type 244 | const stringify7 = build({ 245 | type: 'boolean', 246 | }) 247 | stringify7(true) 248 | expectError(stringify7('a string')) 249 | 250 | // largeArrayMechanism 251 | 252 | build({}, { largeArrayMechanism: 'json-stringify' }) 253 | build({}, { largeArrayMechanism: 'default' }) 254 | expectError(build({} as Schema, { largeArrayMechanism: 'invalid' })) 255 | 256 | build({}, { largeArraySize: 2000 }) 257 | build({}, { largeArraySize: '2e4' }) 258 | build({}, { largeArraySize: 2n }) 259 | expectError(build({} as Schema, { largeArraySize: ['asdf'] })) 260 | --------------------------------------------------------------------------------