1 | <h1 align="center">Fastify</h1>
|
2 |
|
3 | ## Validation and Serialization
|
4 | Fastify 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
|
16 | The 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 |
|
22 | Example:
|
23 | ```js
|
24 | const 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 |
|
53 | const 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 |
|
65 | const queryStringJsonSchema = {
|
66 | name: { type: 'string' },
|
67 | excitement: { type: 'integer' }
|
68 | }
|
69 |
|
70 | */
|
71 |
|
72 |
|
73 | const paramsJsonSchema = {
|
74 | type: 'object',
|
75 | properties: {
|
76 | par1: { type: 'string' },
|
77 | par2: { type: 'number' }
|
78 | }
|
79 | }
|
80 |
|
81 | const headersJsonSchema = {
|
82 | type: 'object',
|
83 | properties: {
|
84 | 'x-foo': { type: 'string' }
|
85 | },
|
86 | required: ['x-foo']
|
87 | }
|
88 |
|
89 | const schema = {
|
90 | body: bodyJsonSchema,
|
91 |
|
92 | querystring: queryStringJsonSchema,
|
93 |
|
94 | params: paramsJsonSchema,
|
95 |
|
96 | headers: headersJsonSchema
|
97 | }
|
98 |
|
99 | fastify.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
|
105 | Thanks 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 |
|
107 | There 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),
|
109 | you 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.
|
111 | To use it you have to `addSchema` with an `$id` having a relative URI fragment which is a simple string that
|
112 | applies only to alphanumeric chars `[A-Za-z0-9]`.
|
113 |
|
114 | Here 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 |
|
126 | More examples:
|
127 |
|
128 | **`$ref-way`** usage examples:
|
129 |
|
130 | ```js
|
131 | fastify.addSchema({
|
132 | $id: 'http://example.com/common.json',
|
133 | type: 'object',
|
134 | properties: {
|
135 | hello: { type: 'string' }
|
136 | }
|
137 | })
|
138 |
|
139 | fastify.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
|
155 | const fastify = require('fastify')()
|
156 |
|
157 | fastify.addSchema({
|
158 | $id: 'greetings',
|
159 | type: 'object',
|
160 | properties: {
|
161 | hello: { type: 'string' }
|
162 | }
|
163 | })
|
164 |
|
165 | fastify.route({
|
166 | method: 'POST',
|
167 | url: '/',
|
168 | schema: {
|
169 | body: 'greetings#'
|
170 | },
|
171 | handler: () => {}
|
172 | })
|
173 |
|
174 | fastify.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 |
|
202 | You can use the shared schema everywhere, as top level schema or nested inside other schemas:
|
203 | ```js
|
204 | const fastify = require('fastify')()
|
205 |
|
206 | fastify.addSchema({
|
207 | $id: 'greetings',
|
208 | type: 'object',
|
209 | properties: {
|
210 | hello: { type: 'string' }
|
211 | }
|
212 | })
|
213 |
|
214 | fastify.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 |
|
233 | The function `getSchemas` returns the shared schemas available in the selected scope:
|
234 | ```js
|
235 | fastify.addSchema({ $id: 'one', my: 'hello' })
|
236 | fastify.get('/', (request, reply) => { reply.send(fastify.getSchemas()) })
|
237 |
|
238 | fastify.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 | ```
|
250 | This 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 |
|
261 | You 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
|
266 | const fastify = require('fastify')({
|
267 | ajv: {
|
268 | plugins: [
|
269 | require('ajv-merge-patch')
|
270 | ]
|
271 | }
|
272 | })
|
273 |
|
274 | fastify.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 |
|
303 | fastify.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 |
|
332 | The `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 |
|
334 | Fastify'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 |
|
345 | This 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 |
|
347 | If 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
|
350 | const fastify = require('fastify')()
|
351 | const Ajv = require('ajv')
|
352 | const 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 | })
|
361 | fastify.setSchemaCompiler(function (schema) {
|
362 | return ajv.compile(schema)
|
363 | })
|
364 |
|
365 | // -------
|
366 | // Alternatively, you can set the schema compiler using the setter property:
|
367 | fastify.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 |
|
374 | The `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 |
|
376 | However, 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 |
|
381 | The examples below are therefore equivalent:
|
382 |
|
383 | ```js
|
384 | const joi = require('joi')
|
385 |
|
386 | // Validation options to match ajv's baseline options used in Fastify
|
387 | const 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 |
|
394 | const joiBodySchema = joi.object().keys({
|
395 | age: joi.number().integer().required(),
|
396 | sub: joi.object().keys({
|
397 | name: joi.string().required()
|
398 | }).required()
|
399 | })
|
400 |
|
401 | const 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...
|
412 | const joiSchemaCompiler = schema => data => joiSchema.validate(data, joiOptions)
|
413 |
|
414 | fastify.post('/the/url', {
|
415 | schema: {
|
416 | body: joiBodySchema
|
417 | },
|
418 | schemaCompiler: joiSchemaCompiler
|
419 | }, handler)
|
420 | ```
|
421 |
|
422 | ```js
|
423 | const yup = require('yup')
|
424 |
|
425 | // Validation options to match ajv's baseline options used in Fastify
|
426 | const yupOptions = {
|
427 | strict: false,
|
428 | abortEarly: false, // return all errors
|
429 | stripUnknown: true, // remove additional properties
|
430 | recursive: true
|
431 | }
|
432 |
|
433 | const yupBodySchema = yup.object({
|
434 | age: yup.number().integer().required(),
|
435 | sub: yup.object().shape({
|
436 | name: yup.string().required()
|
437 | }).required()
|
438 | })
|
439 |
|
440 | const 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 |
|
450 | fastify.post('/the/url', {
|
451 | schema: {
|
452 | body: yupBodySchema
|
453 | },
|
454 | schemaCompiler: yupSchemaCompiler
|
455 | }, handler)
|
456 | ```
|
457 |
|
458 | ##### Validation messages with other validation libraries
|
459 |
|
460 | Fastify'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 |
|
462 | To circumvent this issue, you have 2 main options :
|
463 |
|
464 | 1. 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)
|
465 | 2. or use a custom `errorHandler` to intercept and format your 'custom' validation errors
|
466 |
|
467 | To 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 |
|
472 | A very contrived example of such a custom `errorHandler` handling validation errors is shown below:
|
473 |
|
474 | ```js
|
475 | const 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 |
|
505 | The `schemaResolver` is a function that works together with the `schemaCompiler`: you can't use it
|
506 | with the default schema compiler. This feature is useful when you use complex schemas with `$ref` keyword
|
507 | in your routes and a custom validator.
|
508 |
|
509 | This is needed because all the schemas you add to your custom compiler are unknown to Fastify but it
|
510 | need to resolve the `$ref` paths.
|
511 |
|
512 | ```js
|
513 | const fastify = require('fastify')()
|
514 | const Ajv = require('ajv')
|
515 | const ajv = new Ajv()
|
516 |
|
517 | ajv.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 | })
|
527 | ajv.addSchema({
|
528 | $id: 'urn:schema:response',
|
529 | type: 'object',
|
530 | required: ['foo'],
|
531 | properties: {
|
532 | foo: { $ref: 'urn:schema:foo#/definitions/foo' }
|
533 | }
|
534 | })
|
535 | ajv.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 |
|
544 | fastify.setSchemaCompiler(schema => ajv.compile(schema))
|
545 | fastify.setSchemaResolver((ref) => {
|
546 | return ajv.getSchema(ref).schema
|
547 | })
|
548 |
|
549 | fastify.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
|
566 | Usually 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 |
|
568 | Example:
|
569 | ```js
|
570 | const schema = {
|
571 | response: {
|
572 | 200: {
|
573 | type: 'object',
|
574 | properties: {
|
575 | value: { type: 'string' },
|
576 | otherValue: { type: 'boolean' }
|
577 | }
|
578 | }
|
579 | }
|
580 | }
|
581 |
|
582 | fastify.post('/the/url', { schema }, handler)
|
583 | ```
|
584 |
|
585 | As 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
|
587 | const 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 |
|
605 | fastify.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
|
611 | When 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
|
614 | const schema = {
|
615 | body: {
|
616 | type: 'object',
|
617 | properties: {
|
618 | name: { type: 'string' }
|
619 | },
|
620 | required: ['name']
|
621 | }
|
622 | }
|
623 | ```
|
624 |
|
625 | and 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 |
|
635 | If 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
|
638 | const fastify = Fastify()
|
639 |
|
640 | fastify.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 |
|
648 | You 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
|
651 | fastify.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 |
|
659 | If 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 |
|
661 | Below is an example showing how to add **custom error messages for each property** of a schema by supplying custom AJV options.
|
662 | Inline comments in the schema below describe how to configure it to show a different error message for each case:
|
663 |
|
664 | ```js
|
665 | const fastify = Fastify({
|
666 | ajv: {
|
667 | customOptions: { jsonPointers: true },
|
668 | plugins: [
|
669 | require('ajv-errors')
|
670 | ]
|
671 | }
|
672 | })
|
673 |
|
674 | const 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 |
|
702 | fastify.post('/', { schema, }, (request, reply) => {
|
703 | reply.send({
|
704 | hello: 'world'
|
705 | })
|
706 | })
|
707 | ```
|
708 |
|
709 | If you want to return localized error messages, take a look at [ajv-i18n](https://github.com/epoberezkin/ajv-i18n)
|
710 |
|
711 | ```js
|
712 | const localize = require('ajv-i18n')
|
713 |
|
714 | const fastify = Fastify()
|
715 |
|
716 | const 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 |
|
731 | fastify.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 |
|
743 | JSON Schema has some type of utilities in order to optimize your schemas that,
|
744 | in 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
|
758 | fastify.addSchema({
|
759 | $id: 'sharedAddress',
|
760 | type: 'object',
|
761 | properties: {
|
762 | city: { 'type': 'string' }
|
763 | }
|
764 | })
|
765 |
|
766 | const 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
|
777 | const 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
|
798 | const 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
|
818 | fastify.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 |
|
832 | const 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
|
844 | fastify.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 |
|
857 | const 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)
|