본문으로 건너뛰기

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

import { resolveReference } from '@winglet/json-schema';

const schemaWithInternalRefs = {
type: 'object',
properties: {
user: { $ref: '#/definitions/User' },
admin: { $ref: '#/definitions/User' }
},
definitions: {
User: { type: 'object', properties: { name: { type: 'string' } } }
}
};

// Inline all internal $ref references in one call
const fullyResolvedSchema = 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

Traversal: A single stack-based (non-recursive) depth-first pass driven by a shared generator core. Each node is visited once — filtering, mutation, enter, $ref resolution and child discovery are fused — and revisited once on exit only if it has children. The sync scanner runs the core to completion synchronously; the async scanner awaits callbacks that return thenables.

Key Features:

  • Circular Reference Detection: Prevents infinite loops when schemas reference each other
  • Lazy Reference Resolution: References are resolved only when encountered
  • Reference Isolation: resolved subtrees are cloned at inline time (cloneResolvedSchema, default true) so the original schema is never mutated and repeated $refs are never aliased in the output
  • Schema Mutation: Transform schemas during traversal
  • Filtering: Skip unwanted schema nodes
  • Depth Limiting: Control traversal depth for performance

Performance Considerations:

  • Stack-based traversal (not recursion) handles arbitrarily deep schemas
  • The processed schema is built once, on first getValue, and cached
  • Optional resolver memoization (cacheResolvedReference, default false) avoids redundant resolver calls when the same reference appears repeatedly

This scanner is ideal for complex schema analysis, transformation, documentation generation, form building, and validation preprocessing.