JsonSchemaScanner
A powerful JSON Schema traversal engine that implements depth-first search (DFS) with the Visitor pattern for comprehensive schema analysis and transformation. Provides sophisticated features including $ref reference resolution, circular reference detection, schema mutation, filtering, and depth-limited traversal. The scanner processes schemas in phases (Enter → Reference → ChildEntries → Exit) and maintains internal state for efficient processing and result caching.
Signature
class JsonSchemaScanner
Examples
Basic schema traversal with visitor pattern
import { JsonSchemaScanner } from '@winglet/json-schema';
const schema = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
age: { type: 'number', minimum: 0 },
address: {
$ref: '#/definitions/Address'
}
},
definitions: {
Address: {
type: 'object',
properties: {
street: { type: 'string' },
city: { type: 'string' }
}
}
}
};
const scanner = new JsonSchemaScanner({
visitor: {
enter: (entry) => {
console.log(`Entering: ${entry.path} (${entry.schema.type})`);
},
exit: (entry) => {
console.log(`Exiting: ${entry.path}`);
}
},
options: {
maxDepth: 10
}
});
const processedSchema = scanner.scan(schema).getValue();
Schema transformation with mutation
import { isStringSchema } from '@winglet/json-schema/filter';
const transformScanner = new JsonSchemaScanner({
options: {
mutate: (entry) => {
// Add default titles to string fields (handles both nullable and non-nullable)
if (isStringSchema(entry.schema) && !entry.schema.title) {
return {
...entry.schema,
title: `Field at ${entry.dataPath}`
};
}
}
}
});
const enhancedSchema = transformScanner.scan(originalSchema).getValue();
Reference resolution with custom resolver
const definitions = {
'/schemas/user.json': { type: 'object', properties: { id: { type: 'string' } } },
'/schemas/address.json': { type: 'object', properties: { city: { type: 'string' } } }
};
const resolverScanner = new JsonSchemaScanner({
options: {
resolveReference: (refPath) => {
return definitions[refPath];
},
context: { resolveCount: 0 }
},
visitor: {
enter: (entry, context) => {
if (entry.referenceResolved) {
context.resolveCount++;
}
}
}
});
const resolvedSchema = resolverScanner.scan(schemaWithRefs).getValue();
Conditional processing with filtering
import { isStringSchema, isObjectSchema } from '@winglet/json-schema/filter';
interface AnalysisContext {
stringFieldCount: number;
objectFieldCount: number;
}
const analysisScanner = new JsonSchemaScanner<AnalysisContext>({
options: {
filter: (entry) => {
// Only process non-definition schemas
return !entry.path.includes('/definitions/');
},
context: { stringFieldCount: 0, objectFieldCount: 0 }
},
visitor: {
enter: (entry, context) => {
// Use filter functions to handle both nullable and non-nullable types
if (isStringSchema(entry.schema)) context.stringFieldCount++;
if (isObjectSchema(entry.schema)) context.objectFieldCount++;
}
}
});
analysisScanner.scan(complexSchema);
const stats = analysisScanner.options.context;
console.log(`Found ${stats.stringFieldCount} string fields, ${stats.objectFieldCount} objects`);
Built-in reference resolution utility
const schemaWithInternalRefs = {
type: 'object',
properties: {
user: { $ref: '#/definitions/User' },
admin: { $ref: '#/definitions/User' }
},
definitions: {
User: { type: 'object', properties: { name: { type: 'string' } } }
}
};
// Automatically resolve all internal references
const fullyResolvedSchema = JsonSchemaScanner.resolveReference(schemaWithInternalRefs);
Playground
import { JsonSchemaScanner } from '@winglet/json-schema'; const schema = { type: 'object', properties: { name: { type: 'string', minLength: 1 }, age: { type: 'number', minimum: 0 }, address: { $ref: '#/definitions/Address' } }, definitions: { Address: { type: 'object', properties: { street: { type: 'string' }, city: { type: 'string' } } } } }; const scanner = new JsonSchemaScanner({ visitor: { enter: (entry) => { console.log(`Entering: ${entry.path} (${entry.schema.type})`); }, exit: (entry) => { console.log(`Exiting: ${entry.path}`); } }, options: { maxDepth: 10 } }); const processedSchema = scanner.scan(schema).getValue();
Notes
Processing Phases:
- Enter: Initial node processing, filtering, and mutation
- Reference: $ref resolution and circular reference detection
- ChildEntries: Depth checking and child node discovery
- Exit: Final processing and cleanup
Key Features:
- Circular Reference Detection: Prevents infinite loops when schemas reference each other
- Lazy Reference Resolution: References are resolved only when encountered
- Schema Mutation: Transform schemas during traversal
- Filtering: Skip unwanted schema nodes
- Depth Limiting: Control traversal depth for performance
- Result Caching: Processed schemas are cached for efficiency
Performance Considerations:
- Uses stack-based traversal (not recursion) to handle deep schemas
- Implements copy-on-write for mutations to minimize memory usage
- Caches resolved references to avoid redundant processing
This scanner is ideal for complex schema analysis, transformation, documentation generation, form building, and validation preprocessing.