@cook-step/schema-validator
v0.1.0
Published
JSON Schema validation and compatibility checker for CookStep
Maintainers
Readme
@cook-step/schema-validator
JSON Schema validation and compatibility checker with TypeScript-like error messages.
Features
- 🔍 Deep Compatibility Checking - Validates if schemas are compatible at every level
- 📝 TypeScript-like Errors - Clear, actionable error messages with suggestions
- ⚡ High Performance - Built on AJV, the fastest JSON Schema validator
- 🎯 Zero False Positives - Conservative validation ensures reliability
- 📊 Detailed Reports - Errors, warnings, and suggestions for fixing issues
- 🔧 Fully Typed - Complete TypeScript support with strict types
- ✅ Battle-tested - 306 tests with 79% code coverage
- 🎨 AJV-aligned - Behavior matches AJV's JSON Schema interpretation exactly
Installation
npm install @cook-step/schema-validator
# or
pnpm add @cook-step/schema-validator
# or
yarn add @cook-step/schema-validatorQuick Start
import { SchemaCompatibilityValidator } from "@cook-step/schema-validator";
const validator = new SchemaCompatibilityValidator();
// Define source schema (what your output produces)
const sourceSchema = {
type: "object",
properties: {
email: { type: "string" },
age: { type: "number", minimum: 0 },
},
};
// Define target schema (what the input expects)
const targetSchema = {
type: "object",
properties: {
email: { type: "string", format: "email" }, // Requires email format!
age: { type: "number", minimum: 18 }, // Requires age >= 18!
},
required: ["email", "age"],
};
// Check compatibility
const result = validator.validateCompatibility(sourceSchema, targetSchema);
if (!result.compatible) {
console.log(validator.formatResult(result));
// ❌ Schemas are incompatible (2 errors)
//
// Errors:
// ❌ String must have format 'email' but no format is specified
// Path: /properties/email
// Code: INCOMPATIBLE_FORMAT
// 💡 Suggestion: Add format: 'email' to the source schema
//
// ❌ Range [0, ∞] violates constraint minimum: 18
// Path: /properties/age
// Code: RANGE_VIOLATION
// 💡 Suggestion: Set minimum to 18
}Core Concepts
Schema Compatibility
Two schemas are compatible when data that passes the source schema will also pass the target schema:
- Source Schema: The schema of data being produced (output)
- Target Schema: The schema of data being consumed (input)
- Compatible: Source ⊆ Target (source is a subset of target)
Important Compatibility Rules
This validator follows strict JSON Schema semantics aligned with AJV behavior:
1. Required Properties
Properties that exist but are not marked as required in source are incompatible with target that requires them:
// ❌ Incompatible - source can produce {} without config
const source = {
type: "object",
properties: {
config: { type: "string" },
},
// config is NOT required
};
const target = {
type: "object",
properties: {
config: { type: "string" },
},
required: ["config"], // config IS required
};2. Exclusive Bounds (Numbers)
Exclusive bounds work correctly with real numbers, not just integers:
// ❌ Incompatible - source can produce 0.1, 0.5, 0.999...
const source = { type: "number", exclusiveMinimum: 0 }; // > 0
const target = { type: "number", minimum: 1 }; // >= 13. Empty Schema ({}) Behavior
- An empty schema
{}accepts ANY value (string, number, boolean, null, object, array) - Therefore,
{}is incompatible with schemas that have type constraints - This is the same as
trueschema which also accepts everything
// ❌ Incompatible - source can produce any type, target only accepts strings
const source = {};
const target = { type: "string" };4. Missing Constraints are Incompatible
When source lacks constraints that target requires, they are incompatible:
// ❌ Incompatible - source can produce negative numbers
const source = { type: "number" };
const target = { type: "number", minimum: 0 };
// ❌ Incompatible - source can produce empty strings
const source = { type: "string" };
const target = { type: "string", minLength: 5 };3. Exclusive Bounds for Real Numbers
Exclusive bounds work with real numbers, not just integers:
// exclusiveMinimum: 0 means > 0 (includes 0.1, 0.5, 0.999...)
// minimum: 1 means >= 1
// ❌ Incompatible - source can produce 0.1, 0.5, 0.999 which target rejects
const source = { type: "number", exclusiveMinimum: 0 };
const target = { type: "number", minimum: 1 };4. Const Values are Highly Restrictive
When source has const, it only accepts that specific value:
// ✅ Compatible - source only produces 42, which target accepts
const source = { const: 42 };
const target = { type: "number", minimum: 40, maximum: 50 };
// ❌ Incompatible - different const values
const source = { const: "a" };
const target = { const: "b" };5. Boolean Schemas
trueschema accepts everything (equivalent to{})falseschema rejects everything (compatible with any target as empty set)
// ❌ Incompatible - true accepts everything, target only strings
const source = true;
const target = { type: "string" };
// ✅ Compatible - false rejects everything (empty set ⊆ any set)
const source = false;
const target = { type: "string" };6. OneOf Requires Exactly One Match
The oneOf keyword requires values to match exactly one schema - not zero, not multiple:
// Target oneOf rejects values that match:
// - NONE of the schemas (gap problem)
// - MULTIPLE schemas (overlap problem)
// Example of gap: strings of length 4 match neither schema
{
oneOf: [{ minLength: 5 }, { maxLength: 3 }];
}
// Example of overlap: all strings match both schemas
{
oneOf: [{ type: "string" }, { type: "string", minLength: 0 }];
}Validation Coverage
The validator comprehensively checks:
Primitive Types:
- String (formats, patterns, length constraints)
- Number/Integer (ranges, exclusive bounds, multipleOf)
- Boolean (const, enum)
- Null
- Arrays (items, tuples, length, uniqueItems, contains)
- Objects (properties, required, additionalProperties, dependencies)
Advanced Features:
- Composite schemas (allOf, anyOf, oneOf, not)
- Conditional schemas (if/then/else)
- Nested schemas (recursive validation)
- Mixed constraints (combining multiple validation rules)
- Const and enum values
- Pattern properties
- Property dependencies
Error Types
The validator distinguishes between:
- Errors: Breaking incompatibilities that will cause validation failures
- Warnings: Non-breaking issues that should be addressed for best practices
API Reference
SchemaCompatibilityValidator
class SchemaCompatibilityValidator {
constructor(options?: ValidationOptions);
validateCompatibility(
sourceSchema: JSONSchemaInput,
targetSchema: JSONSchemaInput,
options?: ValidationOptions,
): CompatibilityResult;
formatResult(result: CompatibilityResult): string;
testWithData(
sourceSchema: JSONSchemaInput,
targetSchema: JSONSchemaInput,
testData?: any,
): boolean;
}ValidationOptions
interface ValidationOptions {
strict?: boolean; // Strict validation mode
allowCoercion?: boolean; // Allow type coercion
validateFormats?: boolean; // Validate string formats
maxDepth?: number; // Max recursion depth
allErrors?: boolean; // Collect all errors
}CompatibilityResult
interface CompatibilityResult {
compatible: boolean;
errors: CompatibilityError[];
warnings: CompatibilityWarning[];
metadata?: {
duration: number;
schemasAnalyzed: number;
};
}Validation Examples
String Validation
// Format validation
const source = { type: "string" };
const target = { type: "string", format: "email" };
// ❌ Incompatible - source doesn't guarantee email format
// Pattern validation
const source = { type: "string", pattern: "^[a-z]+$" };
const target = { type: "string", pattern: "^[A-Z]+$" };
// ❌ Incompatible - patterns don't match
// Length constraints
const source = { type: "string", maxLength: 10 };
const target = { type: "string", maxLength: 5 };
// ❌ Incompatible - source allows longer stringsNumber Validation
// Range validation
const source = { type: "number", minimum: 0, maximum: 100 };
const target = { type: "number", minimum: 10, maximum: 90 };
// ❌ Incompatible - source range exceeds target range
// Multiple validation
const source = { type: "number", multipleOf: 10 };
const target = { type: "number", multipleOf: 5 };
// ✅ Compatible - all multiples of 10 are multiples of 5Object Validation
// Required properties
const source = {
type: "object",
properties: {
name: { type: "string" },
},
};
const target = {
type: "object",
properties: {
name: { type: "string" },
email: { type: "string" },
},
required: ["name", "email"],
};
// ❌ Incompatible - missing required property 'email'
// Additional properties
const source = {
type: "object",
additionalProperties: true,
};
const target = {
type: "object",
additionalProperties: false,
};
// ❌ Incompatible - target forbids additional propertiesArray Validation
// Items validation
const source = {
type: "array",
items: { type: "string" },
};
const target = {
type: "array",
items: { type: "number" },
};
// ❌ Incompatible - item types don't match
// Length constraints
const source = {
type: "array",
minItems: 0,
maxItems: 5,
};
const target = {
type: "array",
minItems: 2,
};
// ❌ Incompatible - source allows fewer items than target requiresComposite Schemas
// anyOf validation
const source = { type: "string" };
const target = {
anyOf: [{ type: "string" }, { type: "number" }],
};
// ✅ Compatible - source matches one of the anyOf schemas
// oneOf validation - REQUIRES EXACTLY ONE MATCH
const source = { type: "string" };
const target = {
oneOf: [
{ type: "string", minLength: 5 },
{ type: "string", maxLength: 3 },
],
};
// ❌ Incompatible - source can produce strings of length 4 that match NONE
// Also incompatible if source can produce values matching MULTIPLE schemas
// oneOf with overlapping schemas
const source = { type: "string", minLength: 5 };
const target = {
oneOf: [
{ type: "string", minLength: 10 },
{ type: "string", maxLength: 20 },
],
};
// ❌ Incompatible - strings of length 10-20 match BOTH schemas
// oneOf rejects values that match multiple schemas!
// allOf validation
const source = {
type: "object",
properties: {
name: { type: "string" },
},
};
const target = {
allOf: [
{
type: "object",
properties: {
name: { type: "string" },
},
},
{
type: "object",
required: ["email"],
},
],
};
// ❌ Incompatible - source doesn't satisfy all schemas in allOfError Codes
| Code | Description |
| --------------------------------- | ----------------------------------- |
| INCOMPATIBLE_TYPE | Type mismatch between schemas |
| TYPE_MISMATCH | Cannot convert between types |
| INCOMPATIBLE_FORMAT | String format mismatch |
| PATTERN_MISMATCH | Regular expression pattern mismatch |
| STRING_LENGTH_VIOLATION | String length constraint violation |
| RANGE_VIOLATION | Numeric range constraint violation |
| MULTIPLE_OF_MISMATCH | Multiple constraint mismatch |
| MISSING_REQUIRED_PROPERTY | Required property missing in source |
| PROPERTY_TYPE_MISMATCH | Property type incompatibility |
| ADDITIONAL_PROPERTIES_FORBIDDEN | Additional properties not allowed |
| ARRAY_ITEMS_MISMATCH | Array items incompatibility |
| ARRAY_LENGTH_VIOLATION | Array length constraint violation |
| UNIQUE_ITEMS_MISMATCH | Unique items constraint mismatch |
| ENUM_VALUES_MISMATCH | Enum values incompatibility |
| CONST_VALUE_MISMATCH | Const value mismatch |
Advanced Usage
Custom Formats
const validator = new SchemaCompatibilityValidator();
// Add custom format
validator.addFormat("customId", (value) => {
return /^ID-\d{6}$/.test(value);
});
// Now you can validate with custom format
const source = { type: "string", format: "customId" };
const target = { type: "string", format: "customId" };
// ✅ CompatibleTesting with Real Data
const validator = new SchemaCompatibilityValidator();
// Test if specific data would pass both schemas
const testData = {
email: "[email protected]",
age: 25,
};
const compatible = validator.testWithData(sourceSchema, targetSchema, testData);
// Returns true if data passes both schemasBoolean Schemas
The validator handles boolean schemas correctly:
// true schema - allows everything
const source = true;
const target = { type: "string" };
// ❌ Incompatible - true allows everything, target only strings
// false schema - allows nothing
const source = false;
const target = { type: "string" };
// ✅ Compatible - false allows nothing (empty set ⊆ any set)AJV Alignment
This validator is designed to be fully compatible with AJV's JSON Schema interpretation. Key alignments include:
Strict Subset Validation
The validator ensures that the source schema produces values that are a strict subset of what the target schema accepts. This means:
- If source can produce any value that target rejects → Incompatible
- Empty schemas (
{}ortrue) that accept everything are incompatible with constrained schemas - Missing constraints in source when target has them → Incompatible
Real Number Support
Unlike some validators that assume integer arithmetic, this validator correctly handles real numbers:
exclusiveMinimum: 0includes decimals like 0.1, 0.5, 0.999- Range comparisons work with floating-point precision
- Exclusive bounds are properly validated for continuous number ranges
Const and Enum Optimization
When schemas use const or enum, the validator:
- Skips unnecessary constraint validation (range, pattern, length)
- Validates only the actual value compatibility
- Correctly infers types from const/enum values
Test Coverage
All compatibility rules are validated against actual AJV behavior to ensure correctness. The validator passes 250+ test cases covering:
- Primitive types and their coercion
- String formats and patterns
- Number ranges with exclusive bounds
- Const and enum values
- Boolean schemas
- Complex nested structures
Testing & Quality
- 306 comprehensive tests covering edge cases and real-world scenarios
- 79% code coverage (87% branch coverage)
- Core validators coverage:
- String validation: 98.46%
- Number validation: 86.01%
- Object validation: 80.99%
- Array validation: 79.51%
- All behavior validated against actual AJV implementation
Performance
- Validation typically completes in < 1ms for simple schemas
- Complex nested schemas with 100+ properties: ~5-10ms
- Uses AJV internally for optimal performance
- Caches compiled schemas for repeated validations
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT © Anderson D. Rosa
