Skip to content

Commit

Permalink
Add workaround for wrong schemaPath in ajv errors
Browse files Browse the repository at this point in the history
  • Loading branch information
jirutka committed Jun 15, 2024
1 parent fb81140 commit 5716325
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 2 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"test": "npm run build && npm run eslint && npm run test-cov"
},
"dependencies": {
"@json-schema-tools/traverse": "^1.10.4",
"ajv": "^8.0.0",
"damerau-levenshtein": "^1.0.8",
"fast-json-patch": "^3.1.0",
Expand Down
61 changes: 61 additions & 0 deletions src/ajv-schema-path-workaround.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import traverse from '@json-schema-tools/traverse'
import type { ErrorObject } from 'ajv'
import type { JSONSchema7 as JSONSchema } from 'json-schema'

import { deepClone } from './utils.js'

// This is a workaround for https://github.com/ajv-validator/ajv/issues/512

const SchemaPathSymbol = Symbol('schemaPath')

/**
* Adds {@link SchemaPathSymbol} property with JSON Path to each schema object
* in the given `jsonSchema`. This function **mutates** the given `jsonSchema`!
*/
export function injectPathToSchemas(jsonSchema: JSONSchema, prefix?: string): void {
prefix ??= `${jsonSchema.$id || ''}#`

traverse.default(
jsonSchema,
(schema, _isCycle, path) => {
if (!('$ref' in schema) && !(SchemaPathSymbol in schema)) {
schema[SchemaPathSymbol] = `${prefix}${jsonPathToPointer(path)}`
}
return schema
},
{ mutable: true },
)

if (typeof jsonSchema.$defs === 'object') {
for (const [key, schema] of Object.entries(jsonSchema.$defs)) {
injectPathToSchemas(schema as JSONSchema, `${prefix}/$defs/${key}`)
}
}
}

/** @internal */
export function rewriteSchemaPathInErrors(errors: ErrorObject[], verbose: boolean): ErrorObject[] {
return errors.map(error => {
// The main purpose of this is to remove SchemaPathSymbol key.
const copy = deepClone(error)
if (error.parentSchema && SchemaPathSymbol in error.parentSchema) {
copy.schemaPath = `${error.parentSchema[SchemaPathSymbol]}/${error.keyword}`
if (!verbose) {
delete copy.parentSchema
delete copy.schema
delete copy.data
}
}
return copy
})
}

function jsonPathToPointer(jsonPath: string): string {
if (jsonPath.startsWith('$')) {
jsonPath = jsonPath.slice(1)
}
return jsonPath
.split('.')
.map(s => s.replaceAll(/\[(\d+)\]/g, '/$1'))
.join('/')
}
9 changes: 9 additions & 0 deletions src/ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Ajv2020 } from 'ajv/dist/2020.js'
import { Ajv as AjvJTD } from 'ajv/dist/jtd.js'
import type { ParsedArgs } from 'minimist'

import { injectPathToSchemas } from './ajv-schema-path-workaround.js'
import type { SchemaSpec } from './types.js'
import { getOptions } from './options.js'
import * as util from './utils.js'
Expand All @@ -24,6 +25,11 @@ const AjvClass: { [S in SchemaSpec]?: AjvCore } = {

export default async function (argv: ParsedArgs): Promise<InstanceType<AjvCore>> {
const opts = getOptions(argv)
const isValidate = !argv._[0] || argv._[0] === 'validate'
if (isValidate) {
// verbose is needed for ajv-schema-path-workaround.
opts.verbose = true
}
if (argv.o) {
opts.code.source = true
}
Expand Down Expand Up @@ -52,6 +58,9 @@ export default async function (argv: ParsedArgs): Promise<InstanceType<AjvCore>>
for (const file of util.getFiles(args)) {
const schema = util.readFile(file, fileType)
try {
if (isValidate && method === 'addSchema') {
injectPathToSchemas(schema)
}
ajv[method](schema)
} catch (err) {
console.error(`${fileType} ${file} is invalid`)
Expand Down
5 changes: 4 additions & 1 deletion src/commands/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { AnyValidateFunction } from 'ajv/dist/core.js'
import jsonPatch from 'fast-json-patch'
import type { ParsedArgs } from 'minimist'

import { injectPathToSchemas, rewriteSchemaPathInErrors } from '../ajv-schema-path-workaround.js'
import getAjv from '../ajv.js'
import type { Command } from '../types.js'
import { getFiles, readFile } from '../utils.js'
Expand Down Expand Up @@ -58,8 +59,9 @@ async function execute(argv: ParsedArgs): Promise<boolean> {
}
}
} else {
const errors = rewriteSchemaPathInErrors(validate.errors!, argv.verbose)
console.error(file, 'invalid')
console.error(formatData(argv.errors, validate.errors, ajv))
console.error(formatData(argv.errors, errors, ajv))
}
return validData
}
Expand All @@ -68,6 +70,7 @@ async function execute(argv: ParsedArgs): Promise<boolean> {
function compileSchema(ajv: Ajv, schemaFile: string): AnyValidateFunction {
const schema = readFile(schemaFile, 'schema')
try {
injectPathToSchemas(schema, '#')
return ajv.compile(schema)
} catch (err: any) {
console.error(`schema ${schemaFile} is invalid`)
Expand Down
2 changes: 1 addition & 1 deletion test/validate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ describe('validate', function () {
(error, stdout, stderr) => {
assert(error instanceof Error)
assert.strictEqual(stdout, '')
assertRequiredErrors(stderr, 'schema.json')
assertRequiredErrors(stderr, 'schema.json#')
done()
},
)
Expand Down

0 comments on commit 5716325

Please sign in to comment.