Skip to main content
Data validation ensures that incoming data matches your schema definitions before it reaches your service methods. The Feathers schema system provides hooks for validating both data and queries.

Validation Hooks

The schema system provides two main validation hooks:
  • validateData - Validates data for create, update, and patch methods
  • validateQuery - Validates query parameters for all methods

Basic Data Validation

1

Define Your Schema

Create a schema that describes your data structure:
import { schema } from '@feathersjs/schema'

const userDataSchema = schema({
  $id: 'UserData',
  type: 'object',
  additionalProperties: false,
  required: ['email'],
  properties: {
    email: { type: 'string' },
    password: { type: 'string' }
  }
} as const)
2

Add Validation Hook

Apply the validation hook to your service:
import { validateData } from '@feathersjs/schema'

app.service('users').hooks({
  create: [validateData(userDataSchema)]
})
3

Automatic Validation

The hook automatically validates incoming data and throws BadRequest errors if validation fails.

Validation with Type Coercion

The default AJV instance enables type coercion, automatically converting data types:
import { schema, validateData } from '@feathersjs/schema'

const messageSchema = schema({
  $id: 'Message',
  type: 'object',
  required: ['text', 'read'],
  properties: {
    text: { type: 'string' },
    read: { type: 'boolean' },
    upvotes: { type: 'number' }
  }
} as const)

app.service('messages').hooks({
  create: [validateData(messageSchema)]
})

// Input: { text: 'hi', read: 0, upvotes: '10' }
// After validation: { text: 'hi', read: false, upvotes: 10 }

Method-Specific Validation

Using getDataValidator

Create different validators for create, update, and patch:
import { getDataValidator } from '@feathersjs/schema'
import { Ajv } from '@feathersjs/schema'

const userDataSchema = {
  $id: 'UserData',
  type: 'object',
  required: ['email', 'password'],
  properties: {
    email: { type: 'string' },
    password: { type: 'string' },
    name: { type: 'string' }
  }
} as const

const validators = getDataValidator(userDataSchema, new Ajv())

// validators.create - Uses schema as-is
// validators.update - Same as create
// validators.patch - Same as create but no required fields

app.service('users').hooks({
  create: [validateData(validators.create)],
  update: [validateData(validators.update)],
  patch: [validateData(validators.patch)]
})

Query Validation

Validate query parameters to ensure safe database queries:
import { querySyntax, getValidator } from '@feathersjs/typebox'
import { validateQuery } from '@feathersjs/schema'
import { Type } from '@feathersjs/typebox'
import { Ajv } from '@feathersjs/schema'

const messageProperties = Type.Object({
  text: Type.String(),
  userId: Type.Number()
})

const messageQuerySchema = querySyntax(messageProperties)
const messageQueryValidator = getValidator(messageQuerySchema, new Ajv())

app.service('messages').hooks({
  find: [validateQuery(messageQueryValidator)],
  get: [validateQuery(messageQueryValidator)]
})

Query Operators

The querySyntax helper creates schemas supporting Feathers query operators:
// Supports: $gt, $gte, $lt, $lte, $ne, $in, $nin
const query = {
  age: { $gt: 18, $lte: 65 },
  status: { $in: ['active', 'pending'] },
  email: { $ne: '[email protected]' }
}

Custom Query Extensions

Extend query syntax with custom operators:
import { querySyntax } from '@feathersjs/typebox'
import { Type } from '@feathersjs/typebox'

const userSchema = Type.Object({
  name: Type.String(),
  age: Type.Number()
})

const userQuerySchema = querySyntax(userSchema, {
  // Add custom operators for specific fields
  age: {
    $notNull: Type.Boolean()
  },
  name: {
    $ilike: Type.String()  // Case-insensitive LIKE
  }
})

// Now you can use custom operators
const query = {
  age: { $gt: 10, $notNull: true },
  name: { $ilike: 'Dave' }
}

Validation with Custom AJV

Use a custom AJV instance for advanced validation:
1

Create Custom AJV

import Ajv from 'ajv'
import addFormats from 'ajv-formats'

const customAjv = new Ajv({
  coerceTypes: true,
  addUsedSchema: false,
  removeAdditional: true  // Remove properties not in schema
})
addFormats(customAjv)
2

Add Custom Keywords

// Add custom validation keyword for date conversion
customAjv.addKeyword({
  keyword: 'convert',
  type: 'string',
  compile(schemaVal, parentSchema) {
    return ['date-time', 'date'].includes(parentSchema.format) && schemaVal
      ? function (value, obj) {
          const { parentData, parentDataProperty } = obj
          parentData[parentDataProperty] = new Date(value)
          return true
        }
      : () => true
  }
})
3

Use with Schema

const userSchema = schema({
  $id: 'User',
  type: 'object',
  properties: {
    email: { type: 'string', format: 'email' },
    createdAt: {
      type: 'string',
      format: 'date-time',
      convert: true
    }
  }
} as const, customAjv)

app.service('users').hooks({
  create: [validateData(userSchema)]
})

Validation Errors

Validation failures throw BadRequest errors with detailed information:
try {
  await app.service('users').create({
    // Missing required 'email' field
    password: '12345'
  })
} catch (error) {
  console.log(error.name)     // 'BadRequest'
  console.log(error.code)     // 400
  console.log(error.message)  // Validation error message
  console.log(error.data)     // Array of validation errors
  // [
  //   {
  //     instancePath: '',
  //     keyword: 'required',
  //     message: "must have required property 'email'",
  //     params: { missingProperty: 'email' },
  //     schemaPath: '#/required'
  //   }
  // ]
}

Array Validation

The validateData hook automatically handles array data:
import { validateData } from '@feathersjs/schema'

app.service('users').hooks({
  create: [validateData(userDataValidator)]
})

// Validates each item in the array
await app.service('users').create([
  { email: '[email protected]', password: 'pass1' },
  { email: '[email protected]', password: 'pass2' }
])

Best Practices

Apply validation hooks to prevent invalid data from entering your system:
app.service('users').hooks([
  validateQuery(userQueryValidator),
  validateData(userDataValidator)
])
While convenient, type coercion can hide bugs. Consider disabling it for stricter validation:
const strictAjv = new Ajv({
  coerceTypes: false  // Require exact types
})
Always validate queries, especially with SQL databases:
app.service('users').hooks({
  find: [validateQuery(userQueryValidator)]
})
Prevent unexpected properties in your schemas:
const schema = {
  type: 'object',
  additionalProperties: false,  // Reject unknown properties
  properties: { /* ... */ }
}

Next Steps

Schema Resolvers

Learn how to transform data with resolvers

Schema Overview

Back to schema system overview

Build docs developers (and LLMs) love