Repository: josdejong/svelte-jsoneditor
Pull request #522: feat: add createAjvValidatorAsync
Author: login: iStefo
Created: 2025-02-17T16:45:27Z | Merged: 2025-03-12T20:45:07Z

This PR adds a new function, `createAjvValidatorAsync` to the Ajv plugin.

It behaves like `createAjvValidator`, but uses `ajv.compileAsync` on the schema which allows for resolution of remote schemas through the `loadSchema(uri)` hook of Ajv's `options`.

Due to the async nature of `compileAsync`, the return format changes, which would be a breaking change. So I opted for a second factory function in order to not break existing consumers.

The PR also adds `errorSeverity` as an option.

As you can see, I have refactored the code a bit s.t. as little code as possible is duplicated. It had a bit of an unfortunate impact on the testing code, where I also tried to not duplicate all the existing tests just be cause there now is a second factory function.

Thanks for your work & please let me know if you want anything changed :)


Diff:
diff --git a/README.md b/README.md
index 0a575e4a..5becf4a3 100644
--- a/README.md
+++ b/README.md
@@ -328,7 +328,11 @@ True by default. Only applicable to `'table'` mode. When `true`, nested object p
 validator: function (json: unknown): ValidationError[]
 ```
 
-Validate the JSON document. For example use the built-in JSON Schema validator powered by Ajv:
+Validate the JSON document.
+
+##### Using Ajv for JSON Schema
+
+You can use the built-in JSON Schema validator powered by Ajv:
 
 ```js
 import { createAjvValidator } from 'svelte-jsoneditor'
@@ -336,6 +340,27 @@ import { createAjvValidator } from 'svelte-jsoneditor'
 const validator = createAjvValidator({ schema, schemaDefinitions })
 ```
 
+##### Resolving Remote Schemas
+
+If you want Ajv to load remote schemas through its `loadSchema` function, use the async validator builder like this:
+
+```js
+import { createAjvValidatorAsync } from 'svelte-jsoneditor'
+
+const validator = await createAjvValidatorAsync({
+  schema: {
+    $ref: "/schema.json",
+  },
+  ajvOptions: {
+    loadSchema: function (uri: string) {
+      return fetch(uri).then((response) => response.json());
+    },
+  },
+});
+```
+
+`createAjvValidatorAsync` resolves only after Ajv has loaded and compiled all referenced schemas, or rejects on errors, e.g. if `loadSchema` returns a rejected promise.
+
 #### parser
 
 ```ts
@@ -851,6 +876,7 @@ The library exports a set of utility functions. The exact definitions of those f
     - `keyComboFromEvent`
 - Validation:
   - `createAjvValidator`
+  - `createAjvValidatorAsync`
 - Query languages:
   - `jsonQueryLanguage`
   - `jmespathQueryLanguage`
diff --git a/src/lib/plugins/validator/createAjvValidator.test.ts b/src/lib/plugins/validator/createAjvValidator.test.ts
index 1af6f874..b5b6f021 100644
--- a/src/lib/plugins/validator/createAjvValidator.test.ts
+++ b/src/lib/plugins/validator/createAjvValidator.test.ts
@@ -1,8 +1,8 @@
 import { test, describe } from 'vitest'
 import assert from 'assert'
 import Ajv from 'ajv'
-import { createAjvValidator } from './createAjvValidator.js'
-import type { JSONSchemaDefinitions } from '$lib/types'
+import { createAjvValidator, createAjvValidatorAsync } from './createAjvValidator.js'
+import { ValidationSeverity, type JSONSchemaDefinitions } from '$lib/types'
 
 const schema = {
   title: 'Employee',
@@ -83,9 +83,20 @@ const invalidJson = {
   }
 }
 
-describe('createAjvValidator', () => {
-  test('should create a validate function', () => {
-    const validate = createAjvValidator({ schema, schemaDefinitions })
+describe.each([
+  { name: 'createAjvValidator', create: createAjvValidator },
+  { name: 'createAjvValidatorAsync', create: createAjvValidatorAsync }
+])('$name', ({ create }) => {
+  const options = {
+    schema,
+    schemaDefinitions,
+    ajvOptions: {
+      loadSchema: () => Promise.resolve({})
+    }
+  }
+
+  test('should create a validate function', async () => {
+    const validate = await create(options)
 
     assert.deepStrictEqual(validate(invalidJson), [
       {
@@ -99,12 +110,12 @@ describe('createAjvValidator', () => {
     ])
   })
 
-  test('should pass additional Ajv options', () => {
-    const validate = createAjvValidator({
-      schema,
-      schemaDefinitions,
+  test('should pass additional Ajv options', async () => {
+    const validate = await create({
+      ...options,
       ajvOptions: {
-        allErrors: false
+        allErrors: false,
+        loadSchema: () => Promise.resolve({})
       }
     })
 
@@ -117,11 +128,12 @@ describe('createAjvValidator', () => {
     ])
   })
 
-  test('should apply additional Ajv configuration to the existing Ajv instance', () => {
-    const validate = createAjvValidator({
+  test('should apply additional Ajv configuration to the existing Ajv instance', async () => {
+    const validate = await create({
       schema,
       ajvOptions: {
-        allErrors: false
+        allErrors: false,
+        loadSchema: () => Promise.resolve({})
       },
       onCreateAjv: (ajv) => {
         Object.keys(schemaDefinitions).forEach((ref) => {
@@ -139,14 +151,15 @@ describe('createAjvValidator', () => {
     ])
   })
 
-  test('should provide a custom Ajv instance', () => {
-    const validate = createAjvValidator({
+  test('should provide a custom Ajv instance', async () => {
+    const validate = await create({
       schema,
       onCreateAjv: () => {
         const myAjv = new Ajv({
           allErrors: false,
           verbose: true,
-          $data: true
+          $data: true,
+          loadSchema: () => Promise.resolve({})
         })
 
         Object.keys(schemaDefinitions).forEach((ref) => {
@@ -166,26 +179,29 @@ describe('createAjvValidator', () => {
     ])
   })
 
-  test('should throw an error when providing a wrongly configured Ajv instance', () => {
-    assert.throws(() => {
-      createAjvValidator({
-        schema,
-        onCreateAjv: () => new Ajv({ verbose: false })
-      })
-    }, /Ajv must be configured with the option verbose=true/)
+  test('should throw an error when providing a wrongly configured Ajv instance', async () => {
+    await assert.rejects(
+      async () =>
+        create({
+          schema,
+          onCreateAjv: () => new Ajv({ verbose: false })
+        }),
+      /Ajv must be configured with the option verbose=true/
+    )
   })
 
-  test('should throw an error when providing a schema that contains an error', () => {
+  test('should throw an error when providing a schema that contains an error', async () => {
     const invalidSchema = { type: 'foo' }
 
-    assert.throws(() => {
-      createAjvValidator({
-        schema: invalidSchema
+    await assert.rejects(async () => {
+      await create({
+        schema: invalidSchema,
+        ajvOptions: { loadSchema: () => Promise.resolve({}) }
       })
     }, /schema is invalid: data\/type must be equal to one of the allowed values, data\/type must be array, data\/type must match a schema in anyOf/)
   })
 
-  test('should support draft-07', () => {
+  test('should support draft-07', async () => {
     const schemaDraft07 = {
       $schema: 'http://json-schema.org/draft-07/schema#',
       title: 'My test schema',
@@ -201,7 +217,10 @@ describe('createAjvValidator', () => {
       required: ['userId', 'name']
     }
 
-    const validate = createAjvValidator({ schema: schemaDraft07 })
+    const validate = await create({
+      schema: schemaDraft07,
+      ajvOptions: { loadSchema: () => Promise.resolve({}) }
+    })
 
     const validJson = {
       userId: 1,
@@ -220,4 +239,85 @@ describe('createAjvValidator', () => {
   })
 
   // TODO: test support for draft04, draft-06
+
+  test('should use error severity from options', async () => {
+    const validate = await create({
+      ...options,
+      errorSeverity: ValidationSeverity.error
+    })
+
+    assert.deepStrictEqual(validate(invalidJson), [
+      {
+        path: ['gender'],
+        message: 'should be equal to one of: "male", "female"',
+        severity: 'error'
+      },
+      { path: ['age'], message: 'must be integer', severity: 'error' },
+      { path: ['job'], message: "must have required property 'address'", severity: 'error' },
+      { path: ['job', 'salary'], message: 'must be >= 120', severity: 'error' }
+    ])
+  })
+
+  if (create === createAjvValidator) {
+    test('should return validator synchronously', () => {
+      const validate = createAjvValidator(options)
+
+      assert(typeof validate === 'function', 'validator is a function')
+      assert(Array.isArray(validate(invalidJson)), 'validator is callable and returns an array')
+    })
+  }
+
+  if (create === createAjvValidatorAsync) {
+    test('should resolve remote schema using loadSchema', async () => {
+      let loadSchemaCalled = false
+
+      const loadSchema = async (uri: string) => {
+        assert.strictEqual(uri, '/schema.json')
+
+        loadSchemaCalled = true
+
+        return {
+          $id: uri,
+          type: 'object',
+          properties: {
+            age: { type: 'integer' }
+          }
+        }
+      }
+
+      const validate = await createAjvValidatorAsync({
+        schema: {
+          $ref: '/schema.json'
+        },
+        ajvOptions: {
+          loadSchema
+        }
+      })
+
+      assert.deepStrictEqual(validate(invalidJson), [
+        { path: ['age'], message: 'must be integer', severity: 'warning' }
+      ])
+
+      assert.ok(loadSchemaCalled)
+    })
+
+    test('should reject if loadSchema returns a rejected promise', async () => {
+      const loadSchema = async () => {
+        throw new Error('Failed to load schema: Schema not found: /schema.json')
+      }
+
+      await assert.rejects(
+        async () =>
+          createAjvValidatorAsync({
+            schema: {
+              $ref: '/schema.json'
+            },
+            ajvOptions: {
+              loadSchema
+            }
+          }),
+        /Schema not found: \/schema.json/
+      )
+    })
+  }
 })
diff --git a/src/lib/plugins/validator/createAjvValidator.ts b/src/lib/plugins/validator/createAjvValidator.ts
index 83eee1e0..98360d9a 100644
--- a/src/lib/plugins/validator/createAjvValidator.ts
+++ b/src/lib/plugins/validator/createAjvValidator.ts
@@ -1,62 +1,84 @@
 import type Ajv from 'ajv'
-import type { Options, Schema, ErrorObject } from 'ajv'
+import type { Options, Schema, ErrorObject, ValidateFunction, AsyncSchema } from 'ajv'
 import AjvDist from 'ajv'
 import { parsePath } from 'immutable-json-patch'
 import type { JSONSchema, JSONSchemaDefinitions, ValidationError, Validator } from '$lib/types'
 import { ValidationSeverity } from '$lib/types.js'
 
 export interface AjvValidatorOptions {
+  /**
+   * The JSON schema to validate (required).
+   */
   schema: JSONSchema
+
+  /**
+   * An object containing JSON Schema definitions which can be referenced using $ref.
+   */
   schemaDefinitions?: JSONSchemaDefinitions
+
+  /**
+   * Optional extra options for Ajv.
+   */
   ajvOptions?: Options
+
+  /**
+   * An optional callback function allowing to apply additional configuration on the provided Ajv instance, or return
+   * your own Ajv instance and ignore the provided one.
+   */
   onCreateAjv?: (ajv: Ajv) => Ajv | void
+
+  /**
+   * The severity of the validation error.
+   *
+   * @default ValidationSeverity.warning
+   */
+  errorSeverity?: ValidationSeverity
 }
 
 /**
  * Create a JSON Schema validator powered by Ajv.
- * @param options
- * @property schema
- *                    The JSON schema to validate (required).
- * @property [schemaDefinitions=undefined]
- *                    An object containing JSON Schema definitions
- *                    which can be referenced using $ref
- * @property [ajvOptions=undefined]
- *                    Optional extra options for Ajv
- * @property [onCreateAjv=undefined]
- *                    An optional callback function allowing to apply additional
- *                    configuration on the provided Ajv instance, or return
- *                    your own Ajv instance and ignore the provided one.
- * @return Returns a validation function
  */
 export function createAjvValidator(options: AjvValidatorOptions): Validator {
-  let ajv = createAjvInstance(options)
-  if (options.onCreateAjv !== undefined) {
-    ajv = options.onCreateAjv(ajv) || ajv
-
-    // validate whether ajv is configured correctly (this is needed to enhance error messages)
-    if (ajv.opts.verbose === false) {
-      throw new Error('Ajv must be configured with the option verbose=true')
-    }
-  }
+  const ajv = createAjvInstance(options)
 
   const validateAjv = ajv.compile(options.schema as Schema)
 
-  if (validateAjv.errors) {
-    throw validateAjv.errors[0]
-  }
+  return createValidateFunction(validateAjv, options)
+}
 
-  return function validate(json: unknown): ValidationError[] {
-    validateAjv(json)
-    const ajvErrors = validateAjv.errors || []
+/**
+ * Create a JSON Schema validator powered by Ajv.
+ *
+ * Same as `createAjvValidator`, but allows for remote schema resolution through `ajvOptions`'s `loadSchema(uri)`
+ * function.
+ *
+ * Note that `ajvOptions.loadSchema` *must* be set, or Ajv throws an error on initialization!
+ *
+ * ### Example
+ *
+ *     const validate = await createAjvValidatorAsync({
+ *       schema: {
+ *         $ref: '/schema.json'
+ *       },
+ *       ajvOptions: {
+ *         loadSchema(uri) {
+ *           return fetch(uri).then((res) => res.json())
+ *         }
+ *       }
+ *     })
+ */
+export async function createAjvValidatorAsync(options: AjvValidatorOptions): Promise<Validator> {
+  const ajv = createAjvInstance(options)
 
-    return ajvErrors.map(improveAjvError).map((error) => normalizeAjvError(json, error))
-  }
+  const validateAjv = await ajv.compileAsync(options.schema as AsyncSchema)
+
+  return createValidateFunction(validateAjv, options)
 }
 
 function createAjvInstance(options: AjvValidatorOptions): Ajv {
   const { schemaDefinitions, ajvOptions } = options
 
-  const ajv = new AjvDist({
+  let ajv = new AjvDist({
     allErrors: true,
     verbose: true,
     $data: true,
@@ -69,14 +91,41 @@ function createAjvInstance(options: AjvValidatorOptions): Ajv {
     })
   }
 
+  ajv = options.onCreateAjv?.(ajv) ?? ajv
+
+  // validate whether ajv is configured correctly (this is needed to enhance error messages)
+  if (ajv.opts.verbose === false) {
+    throw new Error('Ajv must be configured with the option verbose=true')
+  }
+
   return ajv
 }
 
-function normalizeAjvError(json: unknown, ajvError: ErrorObject): ValidationError {
+function createValidateFunction(
+  ajvValidator: ValidateFunction<unknown>,
+  options: AjvValidatorOptions
+): Validator {
+  if (ajvValidator.errors) {
+    throw ajvValidator.errors[0]
+  }
+
+  return function validate(json: unknown): ValidationError[] {
+    ajvValidator(json)
+    const ajvErrors = ajvValidator.errors ?? []
+
+    return ajvErrors.map(improveAjvError).map((error) => normalizeAjvError(json, error, options))
+  }
+}
+
+function normalizeAjvError(
+  json: unknown,
+  ajvError: ErrorObject,
+  options: AjvValidatorOptions
+): ValidationError {
   return {
     path: parsePath(json, ajvError.instancePath),
-    message: ajvError.message || 'Unknown error',
-    severity: ValidationSeverity.warning
+    message: ajvError.message ?? 'Unknown error',
+    severity: options.errorSeverity ?? ValidationSeverity.warning
   }
 }
 


Reviews:
user: login: josdejong
body: Thanks Stefan, your PR looks good! I like the way you've refactored both the code and made sure the tests are not duplicated. It makes sense to create a separate function for the async variant since it returnse a Promise, that would be a breaking change and not something you want to deal with in the synchronous case.

I made a few inline comments, can you have a look at those?
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-02-28T09:34:27Z
user: login: iStefo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T20:51:51Z
user: login: iStefo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T20:52:03Z
user: login: josdejong
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-12T15:42:38Z
user: login: josdejong
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-12T15:44:12Z
user: login: josdejong
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-12T20:44:32Z

Review comments:
diff_hunk: @@ -83,9 +83,17 @@ const invalidJson = {
   }
 }
 
-describe('createAjvValidator', () => {
-  test('should create a validate function', () => {
-    const validate = createAjvValidator({ schema, schemaDefinitions })
+function sharedAssertions(factoryFn: typeof createAjvValidator | typeof createAjvValidatorAsync) {
path: src/lib/plugins/validator/createAjvValidator.test.ts
user: login: josdejong
body: Can you replace the solution with a `sharedAssertions` function with using `describe.each` of vitest? Docs: https://vitest.dev/api/#describe-each

Something like:

```js
describe.each([
  { name: 'createAjvValidator', create: createAjvValidator },
  { name: 'createAjvValidatorAsync', create: createAjvValidatorAsync }
])('$name', ({ create }) => {
  // ... the tests ...
})
```

created_at: 2025-02-28T09:31:35Z
author_association: OWNER
diff_hunk: @@ -1,62 +1,84 @@
 import type Ajv from 'ajv'
-import type { Options, Schema, ErrorObject } from 'ajv'
+import type { Options, Schema, ErrorObject, ValidateFunction, AsyncSchema } from 'ajv'
 import AjvDist from 'ajv'
 import { parsePath } from 'immutable-json-patch'
 import type { JSONSchema, JSONSchemaDefinitions, ValidationError, Validator } from '$lib/types'
 import { ValidationSeverity } from '$lib/types.js'
 
 export interface AjvValidatorOptions {
+  /**
+   * The JSON schema to validate (required).
+   */
   schema: JSONSchema
+
+  /**
+   * An object containing JSON Schema definitions which can be referenced using $ref.
+   */
   schemaDefinitions?: JSONSchemaDefinitions
+
+  /**
+   * Optional extra options for Ajv.
+   */
   ajvOptions?: Options
+
+  /**
+   * An optional callback function allowing to apply additional configuration on the provided Ajv instance, or return
+   * your own Ajv instance and ignore the provided one.
+   */
   onCreateAjv?: (ajv: Ajv) => Ajv | void
+
+  /**
+   * The severity of the validation error.
+   *
+   * @default ValidationSeverity.warning
+   */
+  errorSeverity?: ValidationSeverity
 }
 
 /**
  * Create a JSON Schema validator powered by Ajv.
- * @param options
- * @property schema
- *                    The JSON schema to validate (required).
- * @property [schemaDefinitions=undefined]
- *                    An object containing JSON Schema definitions
- *                    which can be referenced using $ref
- * @property [ajvOptions=undefined]
- *                    Optional extra options for Ajv
- * @property [onCreateAjv=undefined]
- *                    An optional callback function allowing to apply additional
- *                    configuration on the provided Ajv instance, or return
- *                    your own Ajv instance and ignore the provided one.
- * @return Returns a validation function
  */
 export function createAjvValidator(options: AjvValidatorOptions): Validator {
-  let ajv = createAjvInstance(options)
-  if (options.onCreateAjv !== undefined) {
-    ajv = options.onCreateAjv(ajv) || ajv
-
-    // validate whether ajv is configured correctly (this is needed to enhance error messages)
-    if (ajv.opts.verbose === false) {
-      throw new Error('Ajv must be configured with the option verbose=true')
-    }
-  }
+  const ajv = createAjvInstance(options)
 
   const validateAjv = ajv.compile(options.schema as Schema)
 
-  if (validateAjv.errors) {
-    throw validateAjv.errors[0]
-  }
+  return createValidateFunction(validateAjv, options)
+}
 
-  return function validate(json: unknown): ValidationError[] {
-    validateAjv(json)
-    const ajvErrors = validateAjv.errors || []
+/**
+ * Create a JSON Schema validator powered by Ajv.
+ *
+ * Same as `createAjvValidator`, but allows for remote schema resolution through `ajvOptions`'s `loadSchema(uri)`
+ * function.
+ *
+ * Note that `ajvOptions.loadSchema` *must* be set, or Ajv throws an error on initialization!
+ *
+ * ### Example
+ *
+ *     const validate = await createAjvValidatorAsync({
+ *       schema: {
+ *         $ref: '/schema.json'
+ *       },
+ *       ajvOptions: {
+ *         loadSchema(uri) {
+ *           return fetch(uri).then((res) => res.json())
+ *         }
+ *       }
+ *     })
+ */
+export async function createAjvValidatorAsync(options: AjvValidatorOptions): Promise<Validator> {
path: src/lib/plugins/validator/createAjvValidator.ts
user: login: josdejong
body:  Can you describe the new function `createAjvValidatorAsync` in the README.md, in the sections "validator" and "Utility functions"?
created_at: 2025-02-28T09:31:58Z
author_association: OWNER
diff_hunk: @@ -1,62 +1,84 @@
 import type Ajv from 'ajv'
-import type { Options, Schema, ErrorObject } from 'ajv'
+import type { Options, Schema, ErrorObject, ValidateFunction, AsyncSchema } from 'ajv'
 import AjvDist from 'ajv'
 import { parsePath } from 'immutable-json-patch'
 import type { JSONSchema, JSONSchemaDefinitions, ValidationError, Validator } from '$lib/types'
 import { ValidationSeverity } from '$lib/types.js'
 
 export interface AjvValidatorOptions {
+  /**
+   * The JSON schema to validate (required).
+   */
   schema: JSONSchema
+
+  /**
+   * An object containing JSON Schema definitions which can be referenced using $ref.
+   */
   schemaDefinitions?: JSONSchemaDefinitions
+
+  /**
+   * Optional extra options for Ajv.
+   */
   ajvOptions?: Options
+
+  /**
+   * An optional callback function allowing to apply additional configuration on the provided Ajv instance, or return
+   * your own Ajv instance and ignore the provided one.
+   */
   onCreateAjv?: (ajv: Ajv) => Ajv | void
+
+  /**
+   * The severity of the validation error.
+   *
+   * @default ValidationSeverity.warning
+   */
+  errorSeverity?: ValidationSeverity
path: src/lib/plugins/validator/createAjvValidator.ts
user: login: josdejong
body: Nice addition 👌 
created_at: 2025-02-28T09:32:29Z
author_association: OWNER
diff_hunk: @@ -83,9 +83,17 @@ const invalidJson = {
   }
 }
 
-describe('createAjvValidator', () => {
-  test('should create a validate function', () => {
-    const validate = createAjvValidator({ schema, schemaDefinitions })
+function sharedAssertions(factoryFn: typeof createAjvValidator | typeof createAjvValidatorAsync) {
path: src/lib/plugins/validator/createAjvValidator.test.ts
user: login: iStefo
body: Nice, I didn't know that this is a thing :)
created_at: 2025-03-03T20:51:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,62 +1,84 @@
 import type Ajv from 'ajv'
-import type { Options, Schema, ErrorObject } from 'ajv'
+import type { Options, Schema, ErrorObject, ValidateFunction, AsyncSchema } from 'ajv'
 import AjvDist from 'ajv'
 import { parsePath } from 'immutable-json-patch'
 import type { JSONSchema, JSONSchemaDefinitions, ValidationError, Validator } from '$lib/types'
 import { ValidationSeverity } from '$lib/types.js'
 
 export interface AjvValidatorOptions {
+  /**
+   * The JSON schema to validate (required).
+   */
   schema: JSONSchema
+
+  /**
+   * An object containing JSON Schema definitions which can be referenced using $ref.
+   */
   schemaDefinitions?: JSONSchemaDefinitions
+
+  /**
+   * Optional extra options for Ajv.
+   */
   ajvOptions?: Options
+
+  /**
+   * An optional callback function allowing to apply additional configuration on the provided Ajv instance, or return
+   * your own Ajv instance and ignore the provided one.
+   */
   onCreateAjv?: (ajv: Ajv) => Ajv | void
+
+  /**
+   * The severity of the validation error.
+   *
+   * @default ValidationSeverity.warning
+   */
+  errorSeverity?: ValidationSeverity
 }
 
 /**
  * Create a JSON Schema validator powered by Ajv.
- * @param options
- * @property schema
- *                    The JSON schema to validate (required).
- * @property [schemaDefinitions=undefined]
- *                    An object containing JSON Schema definitions
- *                    which can be referenced using $ref
- * @property [ajvOptions=undefined]
- *                    Optional extra options for Ajv
- * @property [onCreateAjv=undefined]
- *                    An optional callback function allowing to apply additional
- *                    configuration on the provided Ajv instance, or return
- *                    your own Ajv instance and ignore the provided one.
- * @return Returns a validation function
  */
 export function createAjvValidator(options: AjvValidatorOptions): Validator {
-  let ajv = createAjvInstance(options)
-  if (options.onCreateAjv !== undefined) {
-    ajv = options.onCreateAjv(ajv) || ajv
-
-    // validate whether ajv is configured correctly (this is needed to enhance error messages)
-    if (ajv.opts.verbose === false) {
-      throw new Error('Ajv must be configured with the option verbose=true')
-    }
-  }
+  const ajv = createAjvInstance(options)
 
   const validateAjv = ajv.compile(options.schema as Schema)
 
-  if (validateAjv.errors) {
-    throw validateAjv.errors[0]
-  }
+  return createValidateFunction(validateAjv, options)
+}
 
-  return function validate(json: unknown): ValidationError[] {
-    validateAjv(json)
-    const ajvErrors = validateAjv.errors || []
+/**
+ * Create a JSON Schema validator powered by Ajv.
+ *
+ * Same as `createAjvValidator`, but allows for remote schema resolution through `ajvOptions`'s `loadSchema(uri)`
+ * function.
+ *
+ * Note that `ajvOptions.loadSchema` *must* be set, or Ajv throws an error on initialization!
+ *
+ * ### Example
+ *
+ *     const validate = await createAjvValidatorAsync({
+ *       schema: {
+ *         $ref: '/schema.json'
+ *       },
+ *       ajvOptions: {
+ *         loadSchema(uri) {
+ *           return fetch(uri).then((res) => res.json())
+ *         }
+ *       }
+ *     })
+ */
+export async function createAjvValidatorAsync(options: AjvValidatorOptions): Promise<Validator> {
path: src/lib/plugins/validator/createAjvValidator.ts
user: login: iStefo
body: Good point, I added description and example.
created_at: 2025-03-03T20:52:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,9 +83,17 @@ const invalidJson = {
   }
 }
 
-describe('createAjvValidator', () => {
-  test('should create a validate function', () => {
-    const validate = createAjvValidator({ schema, schemaDefinitions })
+function sharedAssertions(factoryFn: typeof createAjvValidator | typeof createAjvValidatorAsync) {
path: src/lib/plugins/validator/createAjvValidator.test.ts
user: login: josdejong
body: Thanks! Now, I think all tests for `createAjvValidator` use an async in front of it, so we do not know for sure whether `createAjvValidator` returns synchronously, right? Can you please add 1 separate test to verify that `createAjvValidator` works synchronous?
created_at: 2025-03-12T15:42:38Z
author_association: OWNER
diff_hunk: @@ -1,62 +1,84 @@
 import type Ajv from 'ajv'
-import type { Options, Schema, ErrorObject } from 'ajv'
+import type { Options, Schema, ErrorObject, ValidateFunction, AsyncSchema } from 'ajv'
 import AjvDist from 'ajv'
 import { parsePath } from 'immutable-json-patch'
 import type { JSONSchema, JSONSchemaDefinitions, ValidationError, Validator } from '$lib/types'
 import { ValidationSeverity } from '$lib/types.js'
 
 export interface AjvValidatorOptions {
+  /**
+   * The JSON schema to validate (required).
+   */
   schema: JSONSchema
+
+  /**
+   * An object containing JSON Schema definitions which can be referenced using $ref.
+   */
   schemaDefinitions?: JSONSchemaDefinitions
+
+  /**
+   * Optional extra options for Ajv.
+   */
   ajvOptions?: Options
+
+  /**
+   * An optional callback function allowing to apply additional configuration on the provided Ajv instance, or return
+   * your own Ajv instance and ignore the provided one.
+   */
   onCreateAjv?: (ajv: Ajv) => Ajv | void
+
+  /**
+   * The severity of the validation error.
+   *
+   * @default ValidationSeverity.warning
+   */
+  errorSeverity?: ValidationSeverity
 }
 
 /**
  * Create a JSON Schema validator powered by Ajv.
- * @param options
- * @property schema
- *                    The JSON schema to validate (required).
- * @property [schemaDefinitions=undefined]
- *                    An object containing JSON Schema definitions
- *                    which can be referenced using $ref
- * @property [ajvOptions=undefined]
- *                    Optional extra options for Ajv
- * @property [onCreateAjv=undefined]
- *                    An optional callback function allowing to apply additional
- *                    configuration on the provided Ajv instance, or return
- *                    your own Ajv instance and ignore the provided one.
- * @return Returns a validation function
  */
 export function createAjvValidator(options: AjvValidatorOptions): Validator {
-  let ajv = createAjvInstance(options)
-  if (options.onCreateAjv !== undefined) {
-    ajv = options.onCreateAjv(ajv) || ajv
-
-    // validate whether ajv is configured correctly (this is needed to enhance error messages)
-    if (ajv.opts.verbose === false) {
-      throw new Error('Ajv must be configured with the option verbose=true')
-    }
-  }
+  const ajv = createAjvInstance(options)
 
   const validateAjv = ajv.compile(options.schema as Schema)
 
-  if (validateAjv.errors) {
-    throw validateAjv.errors[0]
-  }
+  return createValidateFunction(validateAjv, options)
+}
 
-  return function validate(json: unknown): ValidationError[] {
-    validateAjv(json)
-    const ajvErrors = validateAjv.errors || []
+/**
+ * Create a JSON Schema validator powered by Ajv.
+ *
+ * Same as `createAjvValidator`, but allows for remote schema resolution through `ajvOptions`'s `loadSchema(uri)`
+ * function.
+ *
+ * Note that `ajvOptions.loadSchema` *must* be set, or Ajv throws an error on initialization!
+ *
+ * ### Example
+ *
+ *     const validate = await createAjvValidatorAsync({
+ *       schema: {
+ *         $ref: '/schema.json'
+ *       },
+ *       ajvOptions: {
+ *         loadSchema(uri) {
+ *           return fetch(uri).then((res) => res.json())
+ *         }
+ *       }
+ *     })
+ */
+export async function createAjvValidatorAsync(options: AjvValidatorOptions): Promise<Validator> {
path: src/lib/plugins/validator/createAjvValidator.ts
user: login: josdejong
body: Thanks! Can you please also list `createAjvValidatorAsync` in the Utility functions section? Here:

https://github.com/josdejong/svelte-jsoneditor?tab=readme-ov-file#utility-functions
created_at: 2025-03-12T15:44:12Z
author_association: OWNER

PR comments:
user: login: josdejong
created_at: 2025-02-24T19:05:26Z
body: Thanks Stefan, I'll review your PR as soon as I can.
author_association: OWNER
user: login: iStefo
created_at: 2025-03-03T20:53:08Z
body: Thanks for your feedback! I made the suggested changes and force-pushed the amended commit to keep the history clean. Please tell me in case I should rebase against main.

Edit: I just noticed the force push messed up the "view changes since last review". So: I've also added a second test case to test the error handling of `loadSchema`.
author_association: CONTRIBUTOR
user: login: josdejong
created_at: 2025-03-12T15:46:01Z
body: > Thanks for your feedback! I made the suggested changes and force-pushed the amended commit to keep the history clean. Please tell me in case I should rebase against main.

Thanks for the updates Stefan! Looks good.

I made two more (minor) inline comments, can you have a look at them?

author_association: OWNER
user: login: iStefo
created_at: 2025-03-12T20:07:07Z
body: Valid points, I added your suggestions :)
author_association: CONTRIBUTOR
user: login: josdejong
created_at: 2025-03-12T20:44:49Z
body: Thanks! Merging now.
author_association: OWNER
user: login: josdejong
created_at: 2025-03-12T21:01:54Z
body: Published in `v3.1.0`
author_association: OWNER