Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,24 @@ const app = fastify({
})
```

### Fastify with different JSON Schema version

By providing the `ajv.mode` option
it is possible to select a [different JSON Schema version](https://ajv.js.org/json-schema.html#json-schema-versions)
for newer features.

By default the `draft-07` is used.

```js
const app = fastify({
ajv: {
mode: '2019' // or '2020'
}
})

// app uses Ajv2019 for validation
```

### Fastify with JTD

The [JSON Type Definition](https://jsontypedef.com/) feature is supported by AJV v8.x and you can benefit from it in your Fastify application.
Expand Down
11 changes: 3 additions & 8 deletions lib/validator-compiler.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
'use strict'

const Ajv = require('ajv').default
const AjvJTD = require('ajv/dist/jtd')

const defaultAjvOptions = require('./default-ajv-options')

class ValidatorCompiler {
constructor (externalSchemas, options) {
// This instance of Ajv is private
// it should not be customized or used
if (options.mode === 'JTD') {
this.ajv = new AjvJTD(Object.assign({}, defaultAjvOptions, options.customOptions))
} else {
this.ajv = new Ajv(Object.assign({}, defaultAjvOptions, options.customOptions))
}
const ajvPath = ['JTD', '2019', '2020'].includes(options.mode) ? `ajv/dist/${options.mode.toLowerCase()}` : 'ajv'
Copy link
Member

Choose a reason for hiding this comment

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

I would refactor a bit here, because it is strange to me that we don't support the default version.

I would implement something like:

const supportedModes = new Map()

supportedModes.add('2020', () => require('ajv/dist/2020'))
supportedModes.add('2019', () => require('ajv/dist/2019'))
supportedModes.add('draft-04', () => require('ajv/dist/refs/json-schema-draft-06.json'))

const Ajv = require(ajvPath)
Copy link
Member

Choose a reason for hiding this comment

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

It doesn't look like same as before. Previously, accessing the .default.

Suggested change
const Ajv = require(ajvPath)
const Ajv = require(ajvPath)

Copy link
Author

Choose a reason for hiding this comment

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

This allows using Ajv2019 and Ajv2020 by passing 2019 or 2020 in options.mode.

If options.mode is not one of JTD, 2019, or 2020, ajvPath is "ajv" (the default).

If I'm misunderstanding the question, please help me understand.

Copy link
Member

@climba03003 climba03003 Jun 8, 2025

Choose a reason for hiding this comment

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

I means const Ajv = require('ajv').default, now becomes const Ajv = require('ajv')
Unsure why the .default exist.

Copy link
Author

@jmjf jmjf Jun 8, 2025

Choose a reason for hiding this comment

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

I see. Thanks for explaining. This became a learning opportunity for me.

What I learned is, Ajv is ESM. According to Node docs

When an ES Module contains both named exports and a default export, the result returned by require() is the module namespace object, which places the default export in the .default property, similar to the results returned by import(). To customize what should be returned by require(esm) directly, the ES Module can export the desired value using the string name "module.exports".

In Ajv code, Ajv uses module.exports, so require('ajv') should get the Ajv class, which is the default export. The JTD, 2019, and 2020 versions do the same.

My guess is that, in the past, require().default may have been required to get the default export out of the module namespace object. The JTD code wasn't using .default and worked and this code works without .default.

I don't mind adding a commit to add the .default if wanted.

This morning, I woke up thinking I should typeof options.mode === 'string' to the ternary condition to avoid trying to call toLowerCase on a non-string (would default to plain Ajv if options.mode is not a string or is not one of the three recognized options).

But Array.prototype.includes doesn't coerce types, so non-string won't match and won't attempt to call toLowerCase, so not needed. (Another thing I learned.)

this.ajv = new Ajv(Object.assign({}, defaultAjvOptions, options.customOptions))

let addFormatPlugin = true
if (options.plugins && options.plugins.length > 0) {
Expand Down
32 changes: 18 additions & 14 deletions test/duplicated-id-compile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,23 @@ const fastifyAjvOptionsDefault = Object.freeze({
customOptions: {}
})

test('must not store schema on compile', t => {
t.plan(5)
const factory = AjvCompiler()
const compiler = factory({}, fastifyAjvOptionsDefault)
const postFn = compiler({ schema: postSchema })
const patchFn = compiler({ schema: patchSchema })
for (const mode of [undefined, '2019', '2020']) {
test(`mode: ${mode ?? 'default'}`, async (t) => {
await t.test('must not store schema on compile', t => {
t.plan(5)
const factory = AjvCompiler()
const compiler = factory({}, fastifyAjvOptionsDefault)
const postFn = compiler({ schema: postSchema })
const patchFn = compiler({ schema: patchSchema })

const resultForPost = postFn({})
t.assert.deepStrictEqual(resultForPost, false)
t.assert.deepStrictEqual(postFn.errors[0].keyword, 'required')
t.assert.deepStrictEqual(postFn.errors[0].message, "must have required property 'username'")
const resultForPost = postFn({})
t.assert.deepStrictEqual(resultForPost, false)
t.assert.deepStrictEqual(postFn.errors[0].keyword, 'required')
t.assert.deepStrictEqual(postFn.errors[0].message, "must have required property 'username'")

const resultForPatch = patchFn({})
t.assert.ok(resultForPatch)
t.assert.ok(!patchFn.errors)
})
const resultForPatch = patchFn({})
t.assert.ok(resultForPatch)
t.assert.ok(!patchFn.errors)
})
})
}
Loading