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:
-
Reads all .schema.json files from /schemas/
-
Generates corresponding .d.ts files in src/types/generated/
-
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:
-
npm install to install @types packages
-
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
-
Single source of truth - Schema defines validation, types, and docs
-
Strict by default - Always use additionalProperties: false
-
Descriptive error messages - Use description on every property
-
Defense in depth - Validate at API boundary AND before database writes
-
Align with database - Schema constraints should match CHECK constraints
-
Generate, don't duplicate - Use npm run generate:types for TypeScript
-
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