Skip to content

Commit

Permalink
feat: Accept multiple schemas if external definitions are used
Browse files Browse the repository at this point in the history
* A schema can be a string|object or an array pf them.
* The main schema, if there is such, should be passed as the first one
  to be immediately compiled.
* Fix multivalue argument parsing to accept both comma-delimited items
  and multiple arguments. Unfortunately, data arguments have to be
  separated by "--".
* Accept schemas either as strings or as already parsed objects.
* Update the readme with the extended JSON Schema support.
  • Loading branch information
prantlf committed Mar 5, 2023
1 parent d92083d commit 32d1cab
Show file tree
Hide file tree
Showing 15 changed files with 280 additions and 125 deletions.
65 changes: 34 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ This is a fork of the original project ([zaach/jsonlint](https://github.com/zaac
* Optionally recognizes JavaScript-style comments (CJSON) and single quoted strings (JSON5).
* Optionally ignores trailing commas and reports duplicate object keys as an error.
* Optionally checks that also the expected format matches, including sorted object keys.
* Supports [JSON Schema] drafts 04, 06 and 07.
* Supports [JSON Schema] drafts 04, 06, 07, 2019-09 and 2020-12.
* Supports [JSON Type Definition].
* Offers pretty-printing including comment-stripping and object keys without quotes (JSON5).
* Prefers the native JSON parser if possible to run [7x faster than the custom parser].
* Prefers the native JSON parser if possible to run [10x faster than the custom parser].
* Reports errors with rich additional information. From the JSON Schema validation too.
* Consumes configuration from both command line and [configuration files](configuration).
* Implements JavaScript modules using [UMD] to work in Node.js, in a browser, everywhere.
* Depends on up-to-date npm modules with no installation warnings.
* Small size - 18.7 kB minified, 6.54 kB gzipped, 5.16 kB brotlied.
* Small size - 18.4 kB minified, 6.45 kB gzipped, 5.05 kB brotlied.

**Note:** In comparison with the original project, this package exports only the `parse` method; not the `Parser` object.

Expand Down Expand Up @@ -113,50 +114,42 @@ The input can be checked not only to be a valid JSON, but also to be formatted a

### Usage

Usage: `jsonlint [options] [<file, directory, pattern> ...]`
Usage: `jsonlint [options] [--] [<file, directory, pattern> ...]`

#### Options

-f, --config [file] read options from a custom configuration file
-F, --no-config disable searching for configuration file
-f, --config <file> read options from a custom configuration file
-F, --no-config disable searching for configuration files
-s, --sort-keys sort object keys (not when prettifying)
-E, --extensions [ext] file extensions to process for directory walk
(default: ["json","JSON"])
-E, --extensions <ext...> file extensions to process for directory walk (default: ["json","JSON"])
-i, --in-place overwrite the input files
-j, --diff print difference instead of writing the output
-k, --check check that the input is equal to the output
-t, --indent [num|char] number of spaces or specific characters
to use for indentation (default: 2)
-t, --indent <num|char> number of spaces or specific characters to use for indentation (default: 2)
-c, --compact compact error display
-M, --mode [mode] set other parsing flags according to a format
type (default: "json")
-M, --mode <mode> set other parsing flags according to a format type (default: "json")
-B, --bom ignore the leading UTF-8 byte-order mark
-C, --comments recognize and ignore JavaScript-style comments
-S, --single-quoted-strings support single quotes as string delimiters
-T, --trailing-commas ignore trailing commas in objects and arrays
-D, --no-duplicate-keys report duplicate object keys as an error
-V, --validate [file] JSON Schema file to use for validation
-e, --environment [env] which specification of JSON Schema the
validation file uses
-x, --context [num] line count used as the diff context (default: 3)
-V, --validate <file...> JSON Schema file(s) to use for validation (default: [])
-e, --environment <env> which specification of JSON Schema the validation file uses
-x, --context <num> line count used as the diff context (default: 3)
-l, --log-files print only the parsed file names to stdout
-q, --quiet do not print the parsed json to stdout
-n, --continue continue with other files if an error occurs
-p, --pretty-print prettify the input instead of stringifying
the parsed object
-p, --pretty-print prettify the input instead of stringifying the parsed object
-P, --pretty-print-invalid force pretty-printing even for invalid input
-r, --trailing-newline ensure a line break at the end of the output
-R, --no-trailing-newline ensure no line break at the end of the output
--prune-comments omit comments from the prettified output
--strip-object-keys strip quotes from object keys if possible
(JSON5)
--strip-object-keys strip quotes from object keys if possible (JSON5)
--enforce-double-quotes surrounds all strings with double quotes
--enforce-single-quotes surrounds all strings with single quotes
(JSON5)
--trim-trailing-commas omit trailing commas from objects and arrays
(JSON5)
--enforce-single-quotes surrounds all strings with single quotes (JSON5)
--trim-trailing-commas omit trailing commas from objects and arrays (JSON5)
-v, --version output the version number
-h, --help output usage information
-h, --help display help for command

You can use BASH patterns for including and excluding files (only files).
Patterns are case-sensitive and have to use slashes as directory separators.
Expand All @@ -169,6 +162,9 @@ for JSON Schema validation are "draft-04", "draft-06", "draft-07",
with "json-schema-". JSON Type Definition can be selected by "rfc8927",
"json-type-definition" or "jtd". If not specified, it will be "draft-07".

If you specify schemas using the "-V" parameter, you will have to separate
files to test with "--".

### Configuration

In addition to the command line parameters, the options can be supplied from the following files:
Expand Down Expand Up @@ -274,7 +270,7 @@ The `mode` parameter (string) sets parsing options to match a common format of i

### Schema Validation

You can validate the input against a JSON Schema using the `lib/validator` module. The `validate` method accepts either an earlier parsed JSON data or a string with the JSON input:
You can validate the input against a JSON Schema using the `lib/validator` module. The `compile` method accepts either an earlier parsed JSON Schema or a string with it:

```js
const { compile } = require('@prantlf/jsonlint/lib/validator')
Expand All @@ -283,12 +279,18 @@ const validate = compile('string with JSON Schema')
const parsed = validate('string with JSON data')
```

If a string is passed to the `validate` method, the same options as for parsing JSON data can be passed as the second parameter. Compiling JSON Schema supports the same options as parsing JSON data too (except for `reviver`). They can be passed as the second (object) parameter. The optional second `environment` parameter can be passed either as a string or as an additional property in the options object too:
If a string is passed to the `compile` method, the same options as for parsing JSON data can be passed as the second parameter. Compiling JSON Schema supports the same options as parsing JSON data too (except for `reviver`). They can be passed as the second (object) parameter. The optional second `environment` parameter (the default value is `draft-07`) ) can be passed either as a string or as an additional property in the options object too:

```js
const validate = compile('string with JSON Schema', { environment: 'draft-2020-12' })
```

If you use external definitions in multiple schemas, you have to pass an array of all schemas to `compile`. The `$id` properties have to be set in each sub-schema according to the `$ref` references in the main schema. The main schema is usually sent as the first one to be compiled immediately, so that the errors in any sub-schema would be reported right away:

```js
const validate = compile(['string with main schema', 'string with a sub-schema'])
```

### Pretty-Printing

You can parse a JSON string to an array of tokens and print it back to a string with some changes applied. It can be unification of whitespace, reformatting or stripping comments, for example. (Raw token values must be enabled when tokenizing the JSON input.)
Expand Down Expand Up @@ -372,11 +374,11 @@ If you want to retain comments or whitespace for pretty-printing, for example, s

### Performance

This is a part of an output from the [parser benchmark], when parsing a 4.2 KB formatted string ([package.json](./package.json)) with Node.js 12.14.0:
This is a part of an output from the [parser benchmark], when parsing a 4.68 KB formatted string ([package.json](./package.json)) with Node.js 18.14.2:

jsonlint using native JSON.parse x 97,109 ops/sec ±0.81% (93 runs sampled)
jsonlint using hand-coded parser x 7,256 ops/sec ±0.54% (90 runs sampled)
jsonlint using tokenising parser x 6,387 ops/sec ±0.44% (88 runs sampled)
the standard jsonlint parser x 78,998 ops/sec ±0.48% (95 runs sampled)
the extended jsonlint parser x 7,923 ops/sec ±0.51% (93 runs sampled)
the tokenising jsonlint parser x 6,281 ops/sec ±0.71% (91 runs sampled)

A custom JSON parser is [a lot slower] than the built-in one. However, it is more important to have a [clear error reporting] than the highest speed in scenarios like parsing configuration files. (For better error-reporting, the speed can be preserved by using the native parser initially and re-parsing with another parser only in case of failure.) Features like comments or JSON5 are also helpful in configuration files. Tokens preserve the complete input and can be used for pretty-printing without losing the comments.

Expand Down Expand Up @@ -429,6 +431,7 @@ Licensed under the [MIT License].
[JSON]: https://tools.ietf.org/html/rfc8259
[JSON5]: https://spec.json5.org
[JSON Schema]: https://json-schema.org
[JSON Type Definition]: https://jsontypedef.com/
[UMD]: https://github.com/umdjs/umd
[`Grunt`]: https://gruntjs.com/
[`Gulp`]: http://gulpjs.com/
Expand Down
38 changes: 22 additions & 16 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,30 @@ const { sortObject } = require('./sorter')
const { compile } = require('./validator')
const { description, version } = require('../package')

const collectValues = extension => extension.split(',')
const collectValues = (input, result) => result.concat(input.split(','))

const commander = require('commander')
.name('jsonlint')
.usage('[options] [<file, directory, pattern> ...]')
.description(description)
.option('-f, --config [file]', 'read options from a custom configuration file')
.option('-f, --config <file>', 'read options from a custom configuration file')
.option('-F, --no-config', 'disable searching for configuration files')
.option('-s, --sort-keys', 'sort object keys (not when prettifying)')
.option('-E, --extensions [ext]', 'file extensions to process for directory walk', collectValues, ['json', 'JSON'])
.option('-E, --extensions <ext...>', 'file extensions to process for directory walk', collectValues, ['json', 'JSON'])
.option('-i, --in-place', 'overwrite the input files')
.option('-j, --diff', 'print difference instead of writing the output')
.option('-k, --check', 'check that the input is equal to the output')
.option('-t, --indent [num|char]', 'number of spaces or specific characters to use for indentation', 2)
.option('-t, --indent <num|char>', 'number of spaces or specific characters to use for indentation', 2)
.option('-c, --compact', 'compact error display')
.option('-M, --mode [mode]', 'set other parsing flags according to a format type', 'json')
.option('-M, --mode <mode>', 'set other parsing flags according to a format type', 'json')
.option('-B, --bom', 'ignore the leading UTF-8 byte-order mark')
.option('-C, --comments', 'recognize and ignore JavaScript-style comments')
.option('-S, --single-quoted-strings', 'support single quotes as string delimiters')
.option('-T, --trailing-commas', 'ignore trailing commas in objects and arrays')
.option('-D, --no-duplicate-keys', 'report duplicate object keys as an error')
.option('-V, --validate [file]', 'JSON Schema file to use for validation')
.option('-e, --environment [env]', 'which specification of JSON Schema the validation file uses')
.option('-x, --context [num]', 'line count used as the diff context', 3)
.option('-V, --validate <file...>', 'JSON Schema file(s) to use for validation', collectValues, [])
.option('-e, --environment <env>', 'which specification of JSON Schema the validation file uses')
.option('-x, --context <num>', 'line count used as the diff context', 3)
.option('-l, --log-files', 'print only the parsed file names to stdout')
.option('-q, --quiet', 'do not print the parsed json to stdout')
.option('-n, --continue', 'continue with other files if an error occurs')
Expand All @@ -59,6 +59,9 @@ const commander = require('commander')
console.log('"draft-2019-09" or "draft-2020-12". The environment may be prefixed')
console.log('with "json-schema-". JSON Type Definition can be selected by "rfc8927",')
console.log('"json-type-definition" or "jtd". If not specified, it will be "draft-07".')
console.log()
console.log('If you specify schemas using the "-V" parameter, you will have to separate')
console.log('files to test with "--".')
})
.parse(process.argv)

Expand Down Expand Up @@ -144,16 +147,19 @@ function processContents (source, file) {
allowSingleQuotedStrings: options.singleQuotedStrings,
allowDuplicateObjectKeys: options.duplicateKeys
}
if (options.validate) {
let validate
if (options.validate.length) {
const schemas = options.validate.map((file, index) => {
try {
return readFileSync(file, 'utf8')
} catch (error) {
throw new Error(`Loading the JSON Schema #${index + 1} failed: "${file}".\n${error.message}`)
}
})
parserOptions.environment = options.environment
try {
const schema = readFileSync(normalize(options.validate), 'utf8')
parserOptions.environment = options.environment
validate = compile(schema, parserOptions)
validate = compile(schemas, parserOptions)
} catch (error) {
const message = 'Loading the JSON Schema failed: "' +
options.validate + '".\n' + error.message
throw new Error(message)
throw new Error(`Loading the JSON Schema failed:\n${error.message}`)
}
parsed = validate(source, parserOptions)
} else {
Expand Down
3 changes: 2 additions & 1 deletion lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,8 @@ declare module '@prantlf/jsonlint/lib/validator' {
* with multiple options
* @returns the validator function
*/
function compile (schema: string, environmentOrOptions?: Environment | CompileOptions): Validator
function compile (schema: string | string[] | Record<string, unknown> | Record<string, unknown>[],
environmentOrOptions?: Environment | CompileOptions): Validator
}

declare module '@prantlf/jsonlint/lib/printer' {
Expand Down
27 changes: 16 additions & 11 deletions lib/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,28 +123,33 @@
const Ajv = requireAjv('AjvJTD')
ajv = new Ajv()
} else {
throw new RangeError('Unsupported environment for the JSON Schema validation: "' +
environment + '".')
throw new RangeError(`Unsupported environment for the JSON Schema validation: "${environment}".`)
}
return ajv
}

function compileSchema (ajv, schema, parseOptions) {
let parsed
try {
parsed = jsonlint.parse(schema, parseOptions)
} catch (error) {
error.message = 'Parsing the JSON Schema failed.\n' + error.message
throw error
}
if (!Array.isArray(schema)) schema = [schema]
const [main, ...others] = schema.map((schema, index) => {
if (typeof schema !== 'string') return schema
try {
return jsonlint.parse(schema, parseOptions)
} catch (error) {
error.message = `Parsing the JSON Schema #${index + 1} failed.\n${error.message}`
throw error
}
})
try {
return ajv.compile(parsed)
for (const schema of others) {
ajv.addSchema(schema)
}
return ajv.compile(main)
} catch (originalError) {
const errors = ajv.errors
const betterError = errors
? createError(errors, parsed, schema, parseOptions)
: originalError
betterError.message = 'Compiling the JSON Schema failed.\n' + betterError.message
betterError.message = `Compiling the JSON Schema failed.\n${betterError.message}`
throw betterError
}
}
Expand Down
Loading

1 comment on commit 32d1cab

@prantlf
Copy link
Owner Author

@prantlf prantlf commented on 32d1cab Mar 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes #16.

Please sign in to comment.