validation

ProfPowell's avatarfrom ProfPowell

Validate data with JSON Schema and AJV. Use when validating API requests, form submissions, database inputs, or any data boundaries. Provides deterministic validation with consistent error formats.

⭐0stars🔀0forks📁View on GitHub🕐Updated Jan 3, 2026

When & Why to Use This Skill

This Claude skill provides a comprehensive framework for robust data validation using JSON Schema and the AJV runtime validator. It ensures system-wide data integrity by validating inputs at API boundaries, form submissions, and database layers, while offering advanced features like automatic TypeScript type generation and seamless Express middleware integration.

Use Cases

  • API Request Validation: Implementing middleware to automatically validate incoming JSON payloads, query strings, and URL parameters against strict schemas to prevent malformed data processing.
  • Database Integrity Guardrails: Performing 'defense in depth' validation before database writes to ensure data objects strictly mirror database constraints and business logic.
  • Automated Type Safety: Streamlining development by generating TypeScript declaration files (.d.ts) directly from JSON schemas, maintaining a single source of truth for data structures.
  • Cross-Service Contract Management: Defining and enforcing clear data contracts between microservices or frontend/backend teams using standardized, versionable JSON Schema files.
  • Error Standardization: Providing consistent, machine-readable validation error formats (including paths and keywords) to improve debugging and frontend error messaging.
namevalidation
descriptionValidate data with JSON Schema and AJV. Use when validating API requests, form submissions, database inputs, or any data boundaries. Provides deterministic validation with consistent error formats.
allowed-toolsRead, Write, Edit, Glob, Grep

JSON Schema Validation Skill

Validate data at all boundaries using JSON Schema definitions with AJV runtime validation.


When to Use

  • Creating API endpoints that accept user input
  • Validating form submissions server-side
  • Ensuring data integrity before database writes
  • Defining contracts between services
  • Generating TypeScript types from schemas

Schema File Location

Schemas live in /schemas/ directory with this structure:

schemas/
  common/                    # Shared/reusable schemas
    uuid.schema.json
    error-response.schema.json
    pagination.schema.json
  entities/                  # Domain entity schemas
    user.schema.json         # Full entity
    user.create.schema.json  # Create input (no id/timestamps)
    user.update.schema.json  # Partial update (all optional)
  api/                       # API-specific request schemas
    login.schema.json
    register.schema.json

Schema Naming Convention

Pattern Example Purpose
{entity}.schema.json user.schema.json Full entity with all fields
{entity}.create.schema.json user.create.schema.json Create input (no id, no timestamps)
{entity}.update.schema.json user.update.schema.json Partial update (all fields optional)
{context}.schema.json login.schema.json Context-specific schemas

Schema Authoring

Basic Schema Template

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "entities/user.create",
  "title": "Create User",
  "description": "Schema for creating a new user",
  "type": "object",
  "required": ["email", "password"],
  "properties": {
    "email": {
      "type": "string",
      "format": "email",
      "maxLength": 254,
      "description": "User's email address"
    },
    "password": {
      "type": "string",
      "minLength": 8,
      "maxLength": 128,
      "description": "User's password (8-128 characters)"
    },
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100,
      "description": "User's display name"
    }
  },
  "additionalProperties": false
}

Key Attributes

Attribute Purpose
$id Unique identifier for referencing (e.g., "entities/user.create")
required Array of mandatory field names
additionalProperties: false Reject unknown fields (security)
minProperties: 1 For update schemas - require at least one field

Common Validation Keywords

String Validation:

{
  "type": "string",
  "minLength": 1,
  "maxLength": 255,
  "pattern": "^[a-z0-9-]+$",
  "format": "email"
}

Number Validation:

{
  "type": "integer",
  "minimum": 1,
  "maximum": 100,
  "default": 20
}

Enum Validation:

{
  "type": "string",
  "enum": ["draft", "active", "archived"],
  "default": "draft"
}

Nullable Fields:

{
  "type": ["string", "null"],
  "maxLength": 2000
}

Available Formats

AJV with ajv-formats supports:

  • email - Email address
  • uri - Full URI
  • uuid - UUID v4
  • date - ISO date (YYYY-MM-DD)
  • date-time - ISO datetime
  • time - ISO time
  • ipv4, ipv6 - IP addresses
  • hostname - Hostname

Custom formats (defined in validator.js):

  • phone - E.164 phone format (+1234567890)
  • slug - URL-safe identifier (lowercase, hyphens)

Using Validation Middleware

Import and Apply

import { validateBody, validateQuery, validateParams } from './middleware/validate.js';

// Validate request body
app.post('/api/users',
  validateBody('entities/user.create'),
  createUser
);

// Validate query parameters
app.get('/api/items',
  validateQuery('api/list-items'),
  listItems
);

// Validate path parameters
app.get('/api/users/:id',
  validateParams('common/uuid-param'),
  getUser
);

// Combined validation
app.patch('/api/users/:id',
  validateParams('common/uuid-param'),
  validateBody('entities/user.update'),
  updateUser
);

Error Response Format

Validation failures return:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "path": "/email",
        "message": "Invalid email format",
        "keyword": "format"
      },
      {
        "path": "/password",
        "message": "Must be at least 8 characters",
        "keyword": "minLength"
      }
    ]
  }
}

Status codes:

  • 422 - Body validation failed
  • 400 - Query or params validation failed

Validating in Services

For validation outside middleware (e.g., before database writes):

import { validate } from '../api/middleware/validate.js';

async function createUser(data) {
  // Validate before database insert (defense in depth)
  validate(data, 'entities/user.create');

  // Proceed with insert...
  const result = await query(userQueries.create, [data.email, data.name]);
  return result.rows[0];
}

Query Parameter Coercion

Query strings are always strings. The middleware automatically coerces:

Schema Type Input Result
integer "20" 20
boolean "true" true
array "a,b,c" ["a", "b", "c"]

Example query schema:

{
  "$id": "api/list-items",
  "type": "object",
  "properties": {
    "limit": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100,
      "default": 20
    },
    "offset": {
      "type": "integer",
      "minimum": 0,
      "default": 0
    },
    "status": {
      "type": "string",
      "enum": ["draft", "active", "archived"]
    }
  },
  "additionalProperties": false
}

Generating TypeScript Types

Generate .d.ts files from schemas for JSDoc type checking:

npm run generate:types

How It Works

The script uses json-schema-to-typescript to convert JSON Schema files into TypeScript declaration files:

  1. Reads all .schema.json files from /schemas/
  2. Generates corresponding .d.ts files in src/types/generated/
  3. Types can be imported in JSDoc comments for type checking

Generated Structure

src/types/generated/
  common/
    uuid.d.ts
    error-response.d.ts
    pagination.d.ts
  entities/
    user.d.ts
    user.create.d.ts
    user.update.d.ts
  api/
    login.d.ts
    register.d.ts

Using Generated Types

Import types in JSDoc comments:

/**
 * @typedef {import('./types/generated/entities/user.create').UserCreate} CreateUserInput
 * @typedef {import('./types/generated/entities/user').User} User
 */

/**
 * Create a new user
 * @param {CreateUserInput} data - User creation data
 * @returns {Promise<User>} Created user
 */
async function createUser(data) {
  validate(data, 'entities/user.create');
  // data has full type information from schema
  const result = await db.query(userQueries.create, [data.email, data.password, data.name]);
  return result.rows[0];
}

Regenerating Types

Run npm run generate:types whenever schemas are updated to keep types in sync.


Aligning with Database Constraints

Schema validations should mirror database constraints:

Database Constraint JSON Schema Equivalent
NOT NULL Include in required array
UNIQUE Validate in service layer (not schema)
CHECK (status IN ('a', 'b')) "enum": ["a", "b"]
VARCHAR(255) "maxLength": 255
CHECK (amount > 0) "minimum": 1 (exclusive: "exclusiveMinimum": 0)

OpenAPI Integration

Reference schemas from OpenAPI spec:

# openapi.yaml
paths:
  /users:
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: './schemas/entities/user.create.schema.json'
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: './schemas/entities/user.schema.json'
        '422':
          $ref: '#/components/responses/ValidationError'

Type Checking with tsc

The project uses tsc --checkJs for type checking JavaScript files with JSDoc annotations.

Running Type Check

npm run typecheck

Requirements

Type checking requires:

  1. npm install to install @types packages
  2. Files must have JSDoc type annotations

jsconfig.json Configuration

{
  "compilerOptions": {
    "checkJs": true,
    "strict": true,
    "skipLibCheck": true,
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext"
  },
  "include": ["src/**/*.js", "test/**/*.js"],
  "exclude": ["node_modules"]
}

Template Syntax Handling

Files containing template syntax ({{VARIABLE}}) are processed at project creation time and should be excluded from type checking until the project is generated.

In starter templates:

  • Files like config/index.js contain {{PROJECT_NAME}} placeholders
  • These are valid JavaScript after template processing
  • Type checking runs correctly after npm install in a generated project

Common Type Patterns

// Import Express types
/**
 * @typedef {import('express').Request} Request
 * @typedef {import('express').Response} Response
 * @typedef {import('express').NextFunction} NextFunction
 */

// Type middleware parameters
/**
 * @param {Request} req
 * @param {Response} res
 * @param {NextFunction} next
 */
export function myMiddleware(req, res, next) {
  // ...
}

// Import schema types (after npm run generate:types)
/**
 * @typedef {import('./types/generated/entities/user.create').UserCreate} CreateUserInput
 */

Best Practices

  1. Single source of truth - Schema defines validation, types, and docs
  2. Strict by default - Always use additionalProperties: false
  3. Descriptive error messages - Use description on every property
  4. Defense in depth - Validate at API boundary AND before database writes
  5. Align with database - Schema constraints should match CHECK constraints
  6. Generate, don't duplicate - Use npm run generate:types for TypeScript
  7. Add JSDoc types - All exported functions should have parameter and return types

  • rest-api - API endpoint patterns and HTTP status codes
  • error-handling - Custom error classes including ValidationError
  • forms - Client-side HTML5 validation (UX layer)
  • security - Input sanitization and output encoding
  • typescript-author - TypeScript patterns and Zod alternative