UNPKG

24.6 kBMarkdownView Raw
1<h1 align="center">Fastify</h1>
2
3## Validation and Serialization
4Fastify uses a schema-based approach, and even if it is not mandatory we recommend using [JSON Schema](http://json-schema.org/) to validate your routes and serialize your outputs. Internally, Fastify compiles the schema into a highly performant function.
5
6> ## ⚠ Security Notice
7> Treat the schema definition as application code.
8> As both validation and serialization features dynamically evaluate
9> code with `new Function()`, it is not safe to use
10> user-provided schemas. See [Ajv](http://npm.im/ajv) and
11> [fast-json-stringify](http://npm.im/fast-json-stringify) for more
12> details.
13
14<a name="validation"></a>
15### Validation
16The route validation internally relies upon [Ajv](https://www.npmjs.com/package/ajv), which is a high-performance JSON schema validator. Validating the input is very easy: just add the fields that you need inside the route schema, and you are done! The supported validations are:
17- `body`: validates the body of the request if it is a POST, a PATCH or a PUT.
18- `querystring` or `query`: validates the query string. This can be a complete JSON Schema object (with a `type` property of `'object'` and a `'properties'` object containing parameters) or a simpler variation in which the `type` and `properties` attributes are forgone and the query parameters are listed at the top level (see the example below).
19- `params`: validates the route params.
20- `headers`: validates the request headers.
21
22Example:
23```js
24const bodyJsonSchema = {
25 type: 'object',
26 required: ['requiredKey'],
27 properties: {
28 someKey: { type: 'string' },
29 someOtherKey: { type: 'number' },
30 requiredKey: {
31 type: 'array',
32 maxItems: 3,
33 items: { type: 'integer' }
34 },
35 nullableKey: { type: ['number', 'null'] }, // or { type: 'number', nullable: true }
36 multipleTypesKey: { type: ['boolean', 'number'] },
37 multipleRestrictedTypesKey: {
38 oneOf: [
39 { type: 'string', maxLength: 5 },
40 { type: 'number', minimum: 10 }
41 ]
42 },
43 enumKey: {
44 type: 'string',
45 enum: ['John', 'Foo']
46 },
47 notTypeKey: {
48 not: { type: 'array' }
49 }
50 }
51}
52
53const queryStringJsonSchema = {
54 type: 'object',
55 required: ['name'],
56 properties: {
57 name: { type: 'string' },
58 excitement: { type: 'integer' }
59 }
60}
61
62/* If you don't need required query strings,
63 * A short hand syntax is also there:
64
65const queryStringJsonSchema = {
66 name: { type: 'string' },
67 excitement: { type: 'integer' }
68}
69
70*/
71
72
73const paramsJsonSchema = {
74 type: 'object',
75 properties: {
76 par1: { type: 'string' },
77 par2: { type: 'number' }
78 }
79}
80
81const headersJsonSchema = {
82 type: 'object',
83 properties: {
84 'x-foo': { type: 'string' }
85 },
86 required: ['x-foo']
87}
88
89const schema = {
90 body: bodyJsonSchema,
91
92 querystring: queryStringJsonSchema,
93
94 params: paramsJsonSchema,
95
96 headers: headersJsonSchema
97}
98
99fastify.post('/the/url', { schema }, handler)
100```
101*Note that Ajv will try to [coerce](https://github.com/epoberezkin/ajv#coercing-data-types) the values to the types specified in your schema `type` keywords, both to pass the validation and to use the correctly typed data afterwards.*
102
103<a name="shared-schema"></a>
104#### Adding a shared schema
105Thanks to the `addSchema` API, you can add multiple schemas to the Fastify instance and then reuse them in multiple parts of your application. As usual, this API is encapsulated.
106
107There are two ways to reuse your shared schemas:
108+ **`$ref-way`**: as described in the [standard](https://tools.ietf.org/html/draft-handrews-json-schema-01#section-8),
109you can refer to an external schema. To use it you have to `addSchema` with a valid `$id` absolute URI.
110+ **`replace-way`**: this is a Fastify utility that lets you to substitute some fields with a shared schema.
111To use it you have to `addSchema` with an `$id` having a relative URI fragment which is a simple string that
112applies only to alphanumeric chars `[A-Za-z0-9]`.
113
114Here an overview on _how_ to set an `$id` and _how_ references to it:
115
116+ `replace-way`
117 + `myField: 'foobar#'` will search for a shared schema added with `$id: 'foobar'`
118+ `$ref-way`
119 + `myField: { $ref: '#foo'}` will search for field with `$id: '#foo'` inside the current schema
120 + `myField: { $ref: '#/definitions/foo'}` will search for field `definitions.foo` inside the current schema
121 + `myField: { $ref: 'http://url.com/sh.json#'}` will search for a shared schema added with `$id: 'http://url.com/sh.json'`
122 + `myField: { $ref: 'http://url.com/sh.json#/definitions/foo'}` will search for a shared schema added with `$id: 'http://url.com/sh.json'` and will use the field `definitions.foo`
123 + `myField: { $ref: 'http://url.com/sh.json#foo'}` will search for a shared schema added with `$id: 'http://url.com/sh.json'` and it will look inside of it for object with `$id: '#foo'`
124
125
126More examples:
127
128**`$ref-way`** usage examples:
129
130```js
131fastify.addSchema({
132 $id: 'http://example.com/common.json',
133 type: 'object',
134 properties: {
135 hello: { type: 'string' }
136 }
137})
138
139fastify.route({
140 method: 'POST',
141 url: '/',
142 schema: {
143 body: {
144 type: 'array',
145 items: { $ref: 'http://example.com/common.json#/properties/hello' }
146 }
147 },
148 handler: () => {}
149})
150```
151
152**`replace-way`** usage examples:
153
154```js
155const fastify = require('fastify')()
156
157fastify.addSchema({
158 $id: 'greetings',
159 type: 'object',
160 properties: {
161 hello: { type: 'string' }
162 }
163})
164
165fastify.route({
166 method: 'POST',
167 url: '/',
168 schema: {
169 body: 'greetings#'
170 },
171 handler: () => {}
172})
173
174fastify.register((instance, opts, done) => {
175
176 /**
177 * In children's scope can use schemas defined in upper scope like 'greetings'.
178 * Parent scope can't use the children schemas.
179 */
180 instance.addSchema({
181 $id: 'framework',
182 type: 'object',
183 properties: {
184 fastest: { type: 'string' },
185 hi: 'greetings#'
186 }
187 })
188
189 instance.route({
190 method: 'POST',
191 url: '/sub',
192 schema: {
193 body: 'framework#'
194 },
195 handler: () => {}
196 })
197
198 done()
199})
200```
201
202You can use the shared schema everywhere, as top level schema or nested inside other schemas:
203```js
204const fastify = require('fastify')()
205
206fastify.addSchema({
207 $id: 'greetings',
208 type: 'object',
209 properties: {
210 hello: { type: 'string' }
211 }
212})
213
214fastify.route({
215 method: 'POST',
216 url: '/',
217 schema: {
218 body: {
219 type: 'object',
220 properties: {
221 greeting: 'greetings#',
222 timestamp: { type: 'number' }
223 }
224 }
225 },
226 handler: () => {}
227})
228```
229
230<a name="get-shared-schema"></a>
231#### Retrieving a copy of shared schemas
232
233The function `getSchemas` returns the shared schemas available in the selected scope:
234```js
235fastify.addSchema({ $id: 'one', my: 'hello' })
236fastify.get('/', (request, reply) => { reply.send(fastify.getSchemas()) })
237
238fastify.register((instance, opts, done) => {
239 instance.addSchema({ $id: 'two', my: 'ciao' })
240 instance.get('/sub', (request, reply) => { reply.send(instance.getSchemas()) })
241
242 instance.register((subinstance, opts, done) => {
243 subinstance.addSchema({ $id: 'three', my: 'hola' })
244 subinstance.get('/deep', (request, reply) => { reply.send(subinstance.getSchemas()) })
245 done()
246 })
247 done()
248})
249```
250This example will returns:
251
252| URL | Schemas |
253|-------|---------|
254| / | one |
255| /sub | one, two |
256| /deep | one, two, three |
257
258<a name="ajv-plugins"></a>
259#### Ajv Plugins
260
261You can provide a list of plugins you want to use with Ajv:
262
263> Refer to [`ajv options`](https://github.com/fastify/fastify/blob/master/docs/Server.md#factory-ajv) to check plugins format
264
265```js
266const fastify = require('fastify')({
267 ajv: {
268 plugins: [
269 require('ajv-merge-patch')
270 ]
271 }
272})
273
274fastify.route({
275 method: 'POST',
276 url: '/',
277 schema: {
278 body: {
279 $patch: {
280 source: {
281 type: 'object',
282 properties: {
283 q: {
284 type: 'string'
285 }
286 }
287 },
288 with: [
289 {
290 op: 'add',
291 path: '/properties/q',
292 value: { type: 'number' }
293 }
294 ]
295 }
296 }
297 },
298 handler (req, reply) {
299 reply.send({ ok: 1 })
300 }
301})
302
303fastify.route({
304 method: 'POST',
305 url: '/',
306 schema: {
307 body: {
308 $merge: {
309 source: {
310 type: 'object',
311 properties: {
312 q: {
313 type: 'string'
314 }
315 }
316 },
317 with: {
318 required: ['q']
319 }
320 }
321 }
322 },
323 handler (req, reply) {
324 reply.send({ ok: 1 })
325 }
326})
327```
328
329<a name="schema-compiler"></a>
330#### Schema Compiler
331
332The `schemaCompiler` is a function that returns a function that validates the body, url parameters, headers, and query string. The default `schemaCompiler` returns a function that implements the [ajv](https://ajv.js.org/) validation interface. Fastify uses it internally to speed the validation up.
333
334Fastify's [baseline ajv configuration](https://github.com/epoberezkin/ajv#options-to-modify-validated-data) is:
335
336```js
337{
338 removeAdditional: true, // remove additional properties
339 useDefaults: true, // replace missing properties and items with the values from corresponding default keyword
340 coerceTypes: true, // change data type of data to match type keyword
341 nullable: true // support keyword "nullable" from Open API 3 specification.
342}
343```
344
345This baseline configuration can be modified by providing [`ajv.customOptions`](https://github.com/fastify/fastify/blob/master/docs/Server.md#factory-ajv) to your Fastify factory.
346
347If you want to change or set additional config options, you will need to create your own instance and override the existing one like:
348
349```js
350const fastify = require('fastify')()
351const Ajv = require('ajv')
352const ajv = new Ajv({
353 // the fastify defaults (if needed)
354 removeAdditional: true,
355 useDefaults: true,
356 coerceTypes: true,
357 nullable: true,
358 // any other options
359 // ...
360})
361fastify.setSchemaCompiler(function (schema) {
362 return ajv.compile(schema)
363})
364
365// -------
366// Alternatively, you can set the schema compiler using the setter property:
367fastify.schemaCompiler = function (schema) { return ajv.compile(schema) })
368```
369_**Note:** If you use a custom instance of any validator (even Ajv), you have to add schemas to the validator instead of fastify, since fastify's default validator is no longer used, and fastify's `addSchema` method has no idea what validator you are using._
370
371<a name="using-other-validation-libraries"></a>
372#### Using other validation libraries
373
374The `schemaCompiler` function makes it easy to substitute `ajv` with almost any Javascript validation library ([joi](https://github.com/hapijs/joi/), [yup](https://github.com/jquense/yup/), ...).
375
376However, in order to make your chosen validation engine play well with Fastify's request/response pipeline, the function returned by your `schemaCompiler` function should return an object with either :
377
378* in case of validation failure: an `error` property, filled with an instance of `Error` or a string that describes the validation error
379* in case of validation success: an `value` property, filled with the coerced value that passed the validation
380
381The examples below are therefore equivalent:
382
383```js
384const joi = require('joi')
385
386// Validation options to match ajv's baseline options used in Fastify
387const joiOptions = {
388 abortEarly: false, // return all errors
389 convert: true, // change data type of data to match type keyword
390 allowUnknown : false, // remove additional properties
391 noDefaults: false
392}
393
394const joiBodySchema = joi.object().keys({
395 age: joi.number().integer().required(),
396 sub: joi.object().keys({
397 name: joi.string().required()
398 }).required()
399})
400
401const joiSchemaCompiler = schema => data => {
402 // joi `validate` function returns an object with an error property (if validation failed) and a value property (always present, coerced value if validation was successful)
403 const { error, value } = joiSchema.validate(data, joiOptions)
404 if (error) {
405 return { error }
406 } else {
407 return { value }
408 }
409}
410
411// or more simply...
412const joiSchemaCompiler = schema => data => joiSchema.validate(data, joiOptions)
413
414fastify.post('/the/url', {
415 schema: {
416 body: joiBodySchema
417 },
418 schemaCompiler: joiSchemaCompiler
419}, handler)
420```
421
422```js
423const yup = require('yup')
424
425// Validation options to match ajv's baseline options used in Fastify
426const yupOptions = {
427 strict: false,
428 abortEarly: false, // return all errors
429 stripUnknown: true, // remove additional properties
430 recursive: true
431}
432
433const yupBodySchema = yup.object({
434 age: yup.number().integer().required(),
435 sub: yup.object().shape({
436 name: yup.string().required()
437 }).required()
438})
439
440const yupSchemaCompiler = schema => data => {
441 // with option strict = false, yup `validateSync` function returns the coerced value if validation was successful, or throws if validation failed
442 try {
443 const result = schema.validateSync(data, yupOptions)
444 return { value: result }
445 } catch (e) {
446 return { error: e }
447 }
448}
449
450fastify.post('/the/url', {
451 schema: {
452 body: yupBodySchema
453 },
454 schemaCompiler: yupSchemaCompiler
455}, handler)
456```
457
458##### Validation messages with other validation libraries
459
460Fastify's validation error messages are tightly coupled to the default validation engine: errors returned from `ajv` are eventually run through the `schemaErrorsText` function which is responsible for building human-friendly error messages. However, the `schemaErrorsText` function is written with `ajv` in mind : as a result, you may run into odd or incomplete error messages when using other validation librairies.
461
462To circumvent this issue, you have 2 main options :
463
4641. make sure your validation function (returned by your custom `schemaCompiler`) returns errors in the exact same structure and format as `ajv` (although this could prove to be difficult and tricky due to differences between validation engines)
4652. or use a custom `errorHandler` to intercept and format your 'custom' validation errors
466
467To help you in writing a custom `errorHandler`, Fastify adds 2 properties to all validation errors:
468
469* validation: the content of the `error` property of the object returned by the validation function (returned by your custom `schemaCompiler`)
470* validationContext: the 'context' (body, params, query, headers) where the validation error occurred
471
472A very contrived example of such a custom `errorHandler` handling validation errors is shown below:
473
474```js
475const errorHandler = (error, request, reply) => {
476
477 const statusCode = error.statusCode
478 let response
479
480 const { validation, validationContext } = error
481
482 // check if we have a validation error
483 if (validation) {
484 response = {
485 message: `A validation error occured when validating the ${validationContext}...`, // validationContext will be 'body' or 'params' or 'headers' or 'query'
486 errors: validation // this is the result of your validation library...
487 }
488 } else {
489 response = {
490 message: 'An error occurred...'
491 }
492 }
493
494 // any additional work here, eg. log error
495 // ...
496
497 reply.status(statusCode).send(response)
498
499}
500```
501
502<a name="schema-resolver"></a>
503#### Schema Resolver
504
505The `schemaResolver` is a function that works together with the `schemaCompiler`: you can't use it
506with the default schema compiler. This feature is useful when you use complex schemas with `$ref` keyword
507in your routes and a custom validator.
508
509This is needed because all the schemas you add to your custom compiler are unknown to Fastify but it
510need to resolve the `$ref` paths.
511
512```js
513const fastify = require('fastify')()
514const Ajv = require('ajv')
515const ajv = new Ajv()
516
517ajv.addSchema({
518 $id: 'urn:schema:foo',
519 definitions: {
520 foo: { type: 'string' }
521 },
522 type: 'object',
523 properties: {
524 foo: { $ref: '#/definitions/foo' }
525 }
526})
527ajv.addSchema({
528 $id: 'urn:schema:response',
529 type: 'object',
530 required: ['foo'],
531 properties: {
532 foo: { $ref: 'urn:schema:foo#/definitions/foo' }
533 }
534})
535ajv.addSchema({
536 $id: 'urn:schema:request',
537 type: 'object',
538 required: ['foo'],
539 properties: {
540 foo: { $ref: 'urn:schema:foo#/definitions/foo' }
541 }
542})
543
544fastify.setSchemaCompiler(schema => ajv.compile(schema))
545fastify.setSchemaResolver((ref) => {
546 return ajv.getSchema(ref).schema
547})
548
549fastify.route({
550 method: 'POST',
551 url: '/',
552 schema: {
553 body: { $ref: 'urn:schema:request#' },
554 response: {
555 '2xx': { $ref: 'urn:schema:response#' }
556 }
557 },
558 handler (req, reply) {
559 reply.send({ foo: 'bar' })
560 }
561})
562```
563
564<a name="serialization"></a>
565### Serialization
566Usually you will send your data to the clients via JSON, and Fastify has a powerful tool to help you, [fast-json-stringify](https://www.npmjs.com/package/fast-json-stringify), which is used if you have provided an output schema in the route options. We encourage you to use an output schema, as it will increase your throughput by 100-400% depending on your payload and will prevent accidental disclosure of sensitive information.
567
568Example:
569```js
570const schema = {
571 response: {
572 200: {
573 type: 'object',
574 properties: {
575 value: { type: 'string' },
576 otherValue: { type: 'boolean' }
577 }
578 }
579 }
580}
581
582fastify.post('/the/url', { schema }, handler)
583```
584
585As you can see, the response schema is based on the status code. If you want to use the same schema for multiple status codes, you can use `'2xx'`, for example:
586```js
587const schema = {
588 response: {
589 '2xx': {
590 type: 'object',
591 properties: {
592 value: { type: 'string' },
593 otherValue: { type: 'boolean' }
594 }
595 },
596 201: {
597 type: 'object',
598 properties: {
599 value: { type: 'string' }
600 }
601 }
602 }
603}
604
605fastify.post('/the/url', { schema }, handler)
606```
607
608*If you need a custom serializer in a very specific part of your code, you can set one with `reply.serializer(...)`.*
609
610### Error Handling
611When schema validation fails for a request, Fastify will automtically return a status 400 response including the result from the validator in the payload. As an example, if you have the following schema for your route
612
613```js
614const schema = {
615 body: {
616 type: 'object',
617 properties: {
618 name: { type: 'string' }
619 },
620 required: ['name']
621 }
622}
623```
624
625and fail to satisfy it, the route will immediately return a response with the following payload
626
627```js
628{
629 "statusCode": 400,
630 "error": "Bad Request",
631 "message": "body should have required property 'name'"
632}
633```
634
635If you want to handle errors inside the route, you can specify the `attachValidation` option for your route. If there is a validation error, the `validationError` property of the request will contain the `Error` object with the raw `validation` result as shown below
636
637```js
638const fastify = Fastify()
639
640fastify.post('/', { schema, attachValidation: true }, function (req, reply) {
641 if (req.validationError) {
642 // `req.validationError.validation` contains the raw validation error
643 reply.code(400).send(req.validationError)
644 }
645})
646```
647
648You can also use [setErrorHandler](https://www.fastify.io/docs/latest/Server/#seterrorhandler) to define a custom response for validation errors such as
649
650```js
651fastify.setErrorHandler(function (error, request, reply) {
652 if (error.validation) {
653 // error.validationContext can be on of [body, params, querystring, headers]
654 reply.status(422).send(new Error(`validation failed of the ${error.validationContext}`))
655 }
656})
657```
658
659If you want custom error response in schema without headaches and quickly, you can take a look at [`ajv-errors`](https://github.com/epoberezkin/ajv-errors). Checkout the [example](https://github.com/fastify/example/blob/master/validation-messages/custom-errors-messages.js) usage.
660
661Below is an example showing how to add **custom error messages for each property** of a schema by supplying custom AJV options.
662Inline comments in the schema below describe how to configure it to show a different error message for each case:
663
664```js
665const fastify = Fastify({
666 ajv: {
667 customOptions: { jsonPointers: true },
668 plugins: [
669 require('ajv-errors')
670 ]
671 }
672})
673
674const schema = {
675 body: {
676 type: 'object',
677 properties: {
678 name: {
679 type: 'string',
680 errorMessage: {
681 type: 'Bad name'
682 }
683 },
684 age: {
685 type: 'number',
686 errorMessage: {
687 type: 'Bad age', // specify custom message for
688 min: 'Too young' // all constraints except required
689 }
690 }
691 },
692 required: ['name', 'age'],
693 errorMessage: {
694 required: {
695 name: 'Why no name!', // specify error message for when the
696 age: 'Why no age!' // property is missing from input
697 }
698 }
699 }
700}
701
702fastify.post('/', { schema, }, (request, reply) => {
703 reply.send({
704 hello: 'world'
705 })
706})
707```
708
709If you want to return localized error messages, take a look at [ajv-i18n](https://github.com/epoberezkin/ajv-i18n)
710
711```js
712const localize = require('ajv-i18n')
713
714const fastify = Fastify()
715
716const schema = {
717 body: {
718 type: 'object',
719 properties: {
720 name: {
721 type: 'string',
722 },
723 age: {
724 type: 'number',
725 }
726 },
727 required: ['name', 'age'],
728 }
729}
730
731fastify.setErrorHandler(function (error, request, reply) {
732 if (error.validation) {
733 localize.ru(error.validation)
734 reply.status(400).send(error.validation)
735 return
736 }
737 reply.send(error)
738})
739```
740
741### JSON Schema and Shared Schema support
742
743JSON Schema has some type of utilities in order to optimize your schemas that,
744in conjuction with the Fastify's shared schema, let you reuse all your schemas easily.
745
746| Use Case | Validator | Serializer |
747|-----------------------------------|-----------|------------|
748| shared schema | ✔️ | ✔️ |
749| `$ref` to `$id` | ️️✔️ | ✔️ |
750| `$ref` to `/definitions` | ✔️ | ✔️ |
751| `$ref` to shared schema `$id` | ✔️ | ✔️ |
752| `$ref` to shared schema `/definitions` | ✔️ | ✔️ |
753
754#### Examples
755
756```js
757// Usage of the Shared Schema feature
758fastify.addSchema({
759 $id: 'sharedAddress',
760 type: 'object',
761 properties: {
762 city: { 'type': 'string' }
763 }
764})
765
766const sharedSchema = {
767 type: 'object',
768 properties: {
769 home: 'sharedAddress#',
770 work: 'sharedAddress#'
771 }
772}
773```
774
775```js
776// Usage of $ref to $id in same JSON Schema
777const refToId = {
778 type: 'object',
779 definitions: {
780 foo: {
781 $id: '#address',
782 type: 'object',
783 properties: {
784 city: { 'type': 'string' }
785 }
786 }
787 },
788 properties: {
789 home: { $ref: '#address' },
790 work: { $ref: '#address' }
791 }
792}
793```
794
795
796```js
797// Usage of $ref to /definitions in same JSON Schema
798const refToDefinitions = {
799 type: 'object',
800 definitions: {
801 foo: {
802 $id: '#address',
803 type: 'object',
804 properties: {
805 city: { 'type': 'string' }
806 }
807 }
808 },
809 properties: {
810 home: { $ref: '#/definitions/foo' },
811 work: { $ref: '#/definitions/foo' }
812 }
813}
814```
815
816```js
817// Usage $ref to a shared schema $id as external schema
818fastify.addSchema({
819 $id: 'http://foo/common.json',
820 type: 'object',
821 definitions: {
822 foo: {
823 $id: '#address',
824 type: 'object',
825 properties: {
826 city: { 'type': 'string' }
827 }
828 }
829 }
830})
831
832const refToSharedSchemaId = {
833 type: 'object',
834 properties: {
835 home: { $ref: 'http://foo/common.json#address' },
836 work: { $ref: 'http://foo/common.json#address' }
837 }
838}
839```
840
841
842```js
843// Usage $ref to a shared schema /definitions as external schema
844fastify.addSchema({
845 $id: 'http://foo/common.json',
846 type: 'object',
847 definitions: {
848 foo: {
849 type: 'object',
850 properties: {
851 city: { 'type': 'string' }
852 }
853 }
854 }
855})
856
857const refToSharedSchemaDefinitions = {
858 type: 'object',
859 properties: {
860 home: { $ref: 'http://foo/common.json#/definitions/foo' },
861 work: { $ref: 'http://foo/common.json#/definitions/foo' }
862 }
863}
864```
865
866<a name="resources"></a>
867### Resources
868- [JSON Schema](http://json-schema.org/)
869- [Understanding JSON schema](https://spacetelescope.github.io/understanding-json-schema/)
870- [fast-json-stringify documentation](https://github.com/fastify/fast-json-stringify)
871- [Ajv documentation](https://github.com/epoberezkin/ajv/blob/master/README.md)
872- [Ajv i18n](https://github.com/epoberezkin/ajv-i18n)
873- [Ajv custom errors](https://github.com/epoberezkin/ajv-errors)
874- Custom error handling with core methods with error file dumping [example](https://github.com/fastify/example/tree/master/validation-messages)