# Validation

> 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.

- Skill: `profpowell/validation` (Agent Skill)
- Install (CLI): `npx skillmds@latest add profpowell/validation`
- Raw SKILL.md: https://api.skillmd.com/api/skills/profpowell/validation/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: profpowell (https://skillmd.com/u/profpowell)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/profpowell/validation

---


# 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

```json
{
  "$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:**
```json
{
  "type": "string",
  "minLength": 1,
  "maxLength": 255,
  "pattern": "^[a-z0-9-]+$",
  "format": "email"
}
```

**Number Validation:**
```json
{
  "type": "integer",
  "minimum": 1,
  "maximum": 100,
  "default": 20
}
```

**Enum Validation:**
```json
{
  "type": "string",
  "enum": ["draft", "active", "archived"],
  "default": "draft"
}
```

**Nullable Fields:**
```json
{
  "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

```javascript
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:

```json
{
  "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):

```javascript
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:

```json
{
  "$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:

```bash
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:

```javascript
/**
 * @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:

```yaml
# 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

```bash
npm run typecheck
```

### Requirements

Type checking requires:
1. `npm install` to install @types packages
2. Files must have JSDoc type annotations

### jsconfig.json Configuration

```json
{
  "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

```javascript
// 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

---

## Related Skills

- `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

