Skip to content

Commit ea5a8a2

Browse files
committed
feat: Improve schema error reporting to the level of data parsing
* Include location and data exzerpt. * Use data JSON pointer and append schema JSON pointer too. * Remort errors from schema parsing in the same quality too.
1 parent 4d91a54 commit ea5a8a2

File tree

4 files changed

+149
-42
lines changed

4 files changed

+149
-42
lines changed

README.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ This is a fork of the original package with the following enhancements:
1717
* Optionally recognizes JavaScript-style comments and single quoted strings.
1818
* Optionally ignores trailing commas and reports duplicate object keys as an error.
1919
* Supports [JSON Schema] drafts 04, 06 and 07.
20-
* Prefers the native JSON parser to gain the [best performance], while showing error messages of the same quality.
20+
* Prefers the native JSON parser if possible to run [7x faster than the custom parser].
21+
* Reports errors with rich additional information. From the schema validation too.
2122
* Implements JavaScript modules using [UMD] to work everywhere.
2223
* Depends on up-to-date npm modules with no installation warnings.
23-
* Small size - 17.6 kB minified, 6.1 kB gzipped.
24+
* Small size - 19.4 kB minified, 6.7 kB gzipped.
2425

2526
Integration to the favourite task loaders is provided by the following NPM modules:
2627

@@ -121,6 +122,8 @@ const data3 = parse("{'creative': true /* for creativity */}", {
121122
})
122123
```
123124

125+
Have a look at the [source] of the [on-line page] to see how to use `jsonlint` on web page.
126+
124127
The exported `parse` method is compatible with the native `JSON.parse` method. The second parameter provides the additional functionality:
125128

126129
parse(input, [reviver|options])
@@ -151,17 +154,16 @@ The `mode` parameter (string) sets parsing options to match a common format of i
151154

152155
### Schema Validation
153156

154-
The parsing method returns the parsed object or throws an error. If the parsing succeeds, you can validate the input against a JSON schema using the `lib/validator` module:
157+
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:
155158

156159
```js
157-
const { parse } = require('@prantlf/jsonlint')
158160
const { compile } = require('@prantlf/jsonlint/lib/validator')
159161
const validate = compile('string with JSON schema')
160162
// Throws an error in case of failure.
161-
validate(parse('string with JSON data'))
163+
const parsed = validate('string with JSON data')
162164
```
163165

164-
Compiling JSON schema supports the same options as parsing JSON data (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:
166+
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:
165167

166168
```js
167169
const validate = compile('string with JSON schema', {
@@ -173,11 +175,13 @@ const validate = compile('string with JSON schema', {
173175

174176
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 10.15.3:
175177

176-
the built-in parser x 61,588 ops/sec ±0.75% (80 runs sampled)
177-
the pure jju parser x 11,396 ops/sec ±1.05% (86 runs sampled)
178-
the extended jju parser x 8,221 ops/sec ±0.99% (87 runs sampled)
178+
the built-in parser x 68,212 ops/sec ±0.86% (87 runs sampled)
179+
the pure jju parser x 10,234 ops/sec ±1.08% (89 runs sampled)
180+
the extended jju parser x 10,210 ops/sec ±1.26% (88 runs sampled)
181+
the tokenisable jju parser x 8,832 ops/sec ±0.92% (89 runs sampled)
182+
the tokenising jju parser x 7,911 ops/sec ±1.05% (86 runs sampled)
179183

180-
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. Extending the parser with the support for comments and single-quoted strings does not affect significantly the performance.
184+
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. Extending the parser with the support for comments and single-quoted strings does not affect the performance. Making the parser collect tokens and their locations decreases the performance a bit.
181185

182186
### Error Handling
183187

@@ -237,3 +241,5 @@ Licensed under the MIT license.
237241
[parser benchmark]: ./benchmarks#json-parser-comparison
238242
[a lot slower]: ./benchmarks/results/performance.md#results
239243
[clear error reporting]: ./benchmarks/results/errorReportingQuality.md#results
244+
[on-line page]: http://prantlf.github.com/jsonlint/
245+
[source]: ./web/jsonhint.html

lib/cli.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,6 @@ function parse (source, file) {
6060
allowSingleQuotedStrings: options.singleQuotedStrings,
6161
allowDuplicateObjectKeys: options.duplicateKeys
6262
}
63-
parsed = parser.parse(source, parserOptions)
64-
if (options.sortKeys) {
65-
parsed = sorter.sortObject(parsed)
66-
}
6763
if (options.validate) {
6864
var validate
6965
try {
@@ -75,7 +71,12 @@ function parse (source, file) {
7571
options.validate + '".\n' + error.message
7672
throw new Error(message)
7773
}
78-
validate(parsed)
74+
parsed = validate(source, parserOptions)
75+
} else {
76+
parsed = parser.parse(source, parserOptions)
77+
}
78+
if (options.sortKeys) {
79+
parsed = sorter.sortObject(parsed)
7980
}
8081
return JSON.stringify(parsed, null, options.indent)
8182
} catch (e) {

lib/validator.js

Lines changed: 122 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,48 +28,145 @@
2828
}(this, function (exports, Ajv, jsonlint, requireSchemaDraft) {
2929
'use strict'
3030

31-
function compile (schema, environment) {
32-
var options = {}
33-
if (typeof environment === 'object' && !(environment instanceof String)) {
34-
options = environment
35-
environment = options.environment
31+
function addErrorLocation (problem, input, tokens, dataPath) {
32+
var token = tokens.find(function (token) {
33+
return dataPath === jsonlint.pathToPointer(token.path)
34+
})
35+
if (token) {
36+
var location = token.location.start
37+
var offset = location.offset
38+
var line = location.line
39+
var column = location.column
40+
var texts = jsonlint.getErrorTexts(problem.reason, input, offset, line, column)
41+
problem.message = texts.message
42+
problem.exzerpt = texts.exzerpt
43+
if (texts.pointer) {
44+
problem.pointer = texts.pointer
45+
problem.location = {
46+
start: {
47+
column: column,
48+
line: line,
49+
offset: offset
50+
}
51+
}
52+
}
53+
return true
54+
}
55+
}
56+
57+
function errorToProblem (error, input, tokens) {
58+
var dataPath = error.dataPath
59+
var schemaPath = error.schemaPath
60+
var reason = (dataPath || '/') + ' ' + error.message + '; see ' + schemaPath
61+
var problem = {
62+
reason: reason,
63+
dataPath: dataPath,
64+
schemaPath: schemaPath
65+
}
66+
if (!addErrorLocation(problem, input, tokens, dataPath)) {
67+
problem.message = reason
68+
}
69+
return problem
70+
}
71+
72+
function createError (errors, data, input, options) {
73+
if (!input) {
74+
input = JSON.stringify(data, undefined, 2)
3675
}
76+
if (!options) {
77+
options = {}
78+
}
79+
Object.assign(options, {
80+
tokenLocations: true,
81+
tokenPaths: true
82+
})
83+
var tokens = jsonlint.tokenize(input, options)
84+
// var problems = errors.map(function (error) {
85+
// return errorToProblem(error, input, tokens)
86+
// })
87+
// var message = problems
88+
// .map(function (problem) {
89+
// return problem.message
90+
// })
91+
// .join('\n')
92+
var problem = errorToProblem(errors[0], input, tokens)
93+
var error = new SyntaxError(problem.message)
94+
Object.assign(error, problem)
95+
return error
96+
}
97+
98+
function createAjv (environment) {
99+
var ajvOptions = { jsonPointers: true }
37100
var ajv
38101
if (!environment) {
39-
ajv = new Ajv({ schemaId: 'auto' })
102+
ajvOptions.schemaId = 'auto'
103+
ajv = new Ajv(ajvOptions)
40104
ajv.addMetaSchema(requireSchemaDraft('json-schema-draft-04'))
41105
ajv.addMetaSchema(requireSchemaDraft('json-schema-draft-06'))
42106
} else if (environment === 'json-schema-draft-07') {
43-
ajv = new Ajv()
107+
ajv = new Ajv(ajvOptions)
44108
} else if (environment === 'json-schema-draft-06') {
45-
ajv = new Ajv()
109+
ajv = new Ajv(ajvOptions)
46110
ajv.addMetaSchema(requireSchemaDraft('json-schema-draft-06'))
47111
} else if (environment === 'json-schema-draft-04') {
48-
ajv = new Ajv({ schemaId: 'id' })
112+
ajvOptions.schemaId = 'id'
113+
ajv = new Ajv(ajvOptions)
49114
ajv.addMetaSchema(requireSchemaDraft('json-schema-draft-04'))
50115
} else {
51-
throw new Error('Unsupported environment for the JSON schema validation: "' +
116+
throw new RangeError('Unsupported environment for the JSON schema validation: "' +
52117
environment + '".')
53118
}
54-
var validate
119+
return ajv
120+
}
121+
122+
function compileSchema (ajv, schema, parseOptions) {
123+
var parsed
55124
try {
56-
schema = jsonlint.parse(schema, {
57-
mode: options.mode,
58-
ignoreComments: options.ignoreComments,
59-
ignoreTrailingCommas: options.ignoreTrailingCommas,
60-
allowSingleQuotedStrings: options.allowSingleQuotedStrings,
61-
allowDuplicateObjectKeys: options.allowDuplicateObjectKeys
62-
})
63-
validate = ajv.compile(schema)
125+
parsed = jsonlint.parse(schema, parseOptions)
64126
} catch (error) {
65-
throw new Error('Compiling the JSON schema failed.\n' + error.message)
127+
error.message = 'Parsing the JSON schema failed.\n' + error.message
128+
throw error
129+
}
130+
try {
131+
return ajv.compile(parsed)
132+
} catch (originalError) {
133+
var errors = ajv.errors
134+
var betterError = errors
135+
? createError(errors, parsed, schema, parseOptions)
136+
: originalError
137+
betterError.message = 'Compiling the JSON schema failed.\n' + betterError.message
138+
throw betterError
139+
}
140+
}
141+
142+
function compile (schema, environment) {
143+
var options = {}
144+
if (typeof environment === 'object' && !(environment instanceof String)) {
145+
options = environment
146+
environment = options.environment
66147
}
67-
return function (data) {
68-
var result = validate(data)
69-
if (!result) {
70-
var message = ajv.errorsText(validate.errors)
71-
throw new Error(message)
148+
var ajv = createAjv(environment)
149+
var parseOptions = {
150+
mode: options.mode,
151+
ignoreComments: options.ignoreComments,
152+
ignoreTrailingCommas: options.ignoreTrailingCommas,
153+
allowSingleQuotedStrings: options.allowSingleQuotedStrings,
154+
allowDuplicateObjectKeys: options.allowDuplicateObjectKeys
155+
}
156+
var validate = compileSchema(ajv, schema, parseOptions)
157+
return function (data, input, options) {
158+
if (typeof data === 'string' || data instanceof String) {
159+
options = input
160+
input = data
161+
data = jsonlint.parse(input, options)
162+
} else if (!(typeof input === 'string' || input instanceof String)) {
163+
options = input
164+
input = undefined
165+
}
166+
if (validate(data)) {
167+
return data
72168
}
169+
throw createError(validate.errors, data, input, options)
73170
}
74171
}
75172

web/jsonlint.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,11 +198,14 @@ <h2>Result</h2>
198198
mode === 'json5',
199199
allowDuplicateObjectKeys: document.getElementById('duplicate-object-keys').checked,
200200
}
201-
var parsed = jsonlint.parse(document.getElementById('data').value, parserOptions)
201+
var source = document.getElementById('data').value
202+
var parsed
202203
if (document.getElementById('with-schema').checked) {
203204
var schema = document.getElementById('schema').value
204205
var validate = jsonlintValidator.compile(schema, parserOptions)
205-
validate(parsed)
206+
parsed = validate(source, parserOptions)
207+
} else {
208+
parsed = jsonlint.parse(source, parserOptions)
206209
}
207210
document.getElementById('result').innerText = 'Data is valid!'
208211
document.getElementById('result').className = 'pass'

0 commit comments

Comments
 (0)