Skip to content

Strict graph mode

json-tology enforces strict inline-shape detection at the registry boundary by default. enableStrictGraph, enableInlineWarnings, and enableDuplicateDetection are all on by default. Registering an inline constrained shape or a structural duplicate raises SchemaError at registration time. Pass enableStrictGraph: false to JsonTology.create or new SchemaRegistry to opt out.

Three modes operate on the canonical graph and all rely on the same structural-equality test that drives findDuplicates:

  • Strict throw: any inline constrained shape or structural duplicate is a SchemaError at registration time (default).
  • Warn-on-register: emits logger.warn instead of throwing; active alongside strict mode and also as the standalone mode when strict is off.
  • On-demand audit: registry.findDuplicates() is available at any time regardless of mode.

enableInlineWarnings - registration warnings

Emits logger.warn at registration when inline-object or inline-primitive shapes are found. When enableStrictGraph is true (the default), both warnings and throws are active. When strict mode is off, warnings are the only signal. Requires a logger to be set.

/**
 * enableInlineWarnings — gentle nudges at registration time.
 *
 * With `enableInlineWarnings: true`, the registry emits `logger.warn` when
 * an inline-object or inline-primitive shape is found. No throws — registration
 * still succeeds. Requires a logger to be set.
 *
 * Use when you want passive feedback during development without breaking builds.
 *
 * Demonstrates: inline shape registers silently without a logger; with a
 * logger the warning fires but registration succeeds.
 */

import { SchemaRegistry } from '../../../src/modules/registry/SchemaRegistry.js';

// A schema with an inline constrained shape on `isbn`
const BookWithInlineIsbn = {
  '$id': 'urn:bookstore:BookInlineWarning',
  'properties': {
    'isbn': {
      // inline constraint — would warn
      'pattern': '^\\d{13}$',
      'type': 'string'
    },
    'title': { 'type': 'string' }
  },
  'type': 'object'
} as const;

const warnings: string[] = [];

// Provide a minimal logger to capture warn calls
const noop = (): void => {
  /* ignore */
};

// enableStrictGraph: false — this example demonstrates warn-only mode where
// inline shapes are accepted (not thrown); strict checking is off so the
// warning path is exercised instead.
const registry = new SchemaRegistry({
  'enableInlineWarnings': true,
  'enableStrictGraph': false,
  'logger': {
    'debug': noop,
    'error': noop,
    'fatal': noop,
    'info': noop,
    'trace': noop,
    'warn': (msg: string) => {
      warnings.push(msg);
    }
  }
});

// Registration succeeds — no throw in warning mode
registry.set(BookWithInlineIsbn);

console.assert(
  registry.has(BookWithInlineIsbn.$id),
  'schema registered despite inline shape (warn-only mode)'
);

// At least one warning was emitted for the inline isbn shape
console.assert(
  warnings.length > 0,
  'logger.warn was called for the inline constrained shape'
);

console.log('enableInlineWarnings: registered:', registry.has(BookWithInlineIsbn.$id), '| warnings emitted:', warnings.length);
Output
Press Execute to run this example against the real library.

Pass enableInlineWarnings: false explicitly to suppress warnings when strict mode is also off.


enableDuplicateDetection - auto-run at registration

Runs findDuplicates() after each schema is registered and emits logger.warn if duplicates are found. Active by default alongside strict mode.

/**
 * enableDuplicateDetection — auto-run findDuplicates after each registration.
 *
 * With `enableDuplicateDetection: true`, the registry runs `findDuplicates()`
 * after each schema is registered and emits `logger.warn` if duplicates are
 * found. Useful for continuous detection of regressions after some schemas
 * have already been extracted to named types.
 *
 * Demonstrates: two structurally identical ISBN shapes cause a duplicate
 * warning; `findDuplicates()` returns the pair.
 */

import { SchemaRegistry } from '../../../src/modules/registry/SchemaRegistry.js';

// The canonical named ISBN primitive
const IsbnSchema = {
  '$id': 'urn:bookstore:Isbn',
  'pattern': '^\\d{13}$',
  'type': 'string'
} as const;

// A schema that inlines the same ISBN constraint instead of $ref-ing IsbnSchema
const BookWithInlineIsbn = {
  '$id': 'urn:bookstore:BookDuplicateDetection',
  'properties': {
    'isbn': {
      // structurally identical to IsbnSchema
      'pattern': '^\\d{13}$',
      'type': 'string'
    },
    'title': { 'type': 'string' }
  },
  'type': 'object'
} as const;

const warnings: string[] = [];

const noop = (): void => {
  /* ignore */
};

// enableStrictGraph: false — this example demonstrates duplicate-detection warn
// mode where inline shapes are accepted (not thrown); strict checking is off
// so the duplicate-detection warning path is exercised instead of throwing.
const registry = new SchemaRegistry({
  'enableDuplicateDetection': true,
  'enableStrictGraph': false,
  'logger': {
    'debug': noop,
    'error': noop,
    'fatal': noop,
    'info': noop,
    'trace': noop,
    'warn': (msg: string) => {
      warnings.push(msg);
    }
  }
});

registry.set(IsbnSchema);
// triggers duplicate detection
registry.set(BookWithInlineIsbn);

// At least one warning was emitted for the duplicated inline shape
console.assert(
  warnings.length > 0,
  'duplicate detection emits logger.warn for structurally identical inline shape'
);

// Manual findDuplicates() confirms the same result
const duplicates = registry.findDuplicates();

console.assert(duplicates.length > 0, 'findDuplicates returns the inline duplicate pair');
const firstDup = duplicates[0];

if (firstDup === undefined) {
  throw new Error('expected duplicate entry');
}

console.assert(
  firstDup.equivalentTo === IsbnSchema.$id,
  'duplicate points back to the named IsbnSchema'
);

console.log('enableDuplicateDetection: warnings:', warnings.length, '| duplicates:', duplicates.length, '| equivalentTo:', duplicates[0]?.equivalentTo);
Output
Press Execute to run this example against the real library.

Pass enableDuplicateDetection: false to disable automatic duplicate scanning at registration time.


enableStrictGraph - CI enforcement (default)

Promotes duplicate and inline-constraint detection to SchemaError throws. Every sub-schema must be either:

  1. A { $ref: registeredSchemaId } reference
  2. A bare base type with no constraint keywords: { type: 'string' }, { type: 'integer' }, { type: 'boolean' }, { type: 'array', items: <allowed> }
  3. Declared in the schema's own $defs namespace (the schema's internal ontology)

Inline constrained shapes - objects with properties, primitives with pattern/format/minimum/etc., array items with constraints - are all registration errors.

/**
 * Advanced Example 06 — enableStrictGraph mode
 * Demonstrates: strict graph checking is the default; consumers opt out to relax.
 *
 * Strict mode (the default, enableStrictGraph: true) rejects inline primitive
 * constraints such as { pattern: ..., type: 'string' } as properties — each
 * constrained primitive must be a named schema registered with a $id.
 *
 * Permissive mode (enableStrictGraph: false) accepts inline shapes silently;
 * use this only for test fixtures or migration scaffolding.
 */

import { SchemaRegistry } from '../../../src/modules/registry/SchemaRegistry.js';

const SchemaWithInlineShape = {
  '$id': 'urn:example:Book',
  'properties': {
    'isbn': {
      'pattern': '^\\d{13}$',
      'type': 'string'
    },
    'title': { 'type': 'string' }
  },
  'type': 'object'
} as const;

// Strict mode (default): inline shapes throw SchemaError
const strictRegistry = new SchemaRegistry();

try {
  strictRegistry.set(SchemaWithInlineShape);
  console.log('Strict mode: registration succeeded (unexpected)');
} catch (error) {
  const schemaErr = error as { 'code'?: string;
    'message'?: string };

  console.log(`Strict mode: registration threw [${schemaErr.code}]`);
  console.log('  Reason:', schemaErr.message?.split(':').slice(1)
    .join(':')
    .trim()
    .split(';')
    .at(0)
    ?.trim() ?? '');
}

// Permissive mode (opt-out): inline shapes register silently
const permissiveRegistry = new SchemaRegistry({ 'enableStrictGraph': false });

permissiveRegistry.set(SchemaWithInlineShape);
console.log('Permissive mode: registration succeeded (inline shapes are accepted)');
Output
Press Execute to run this example against the real library.

What's allowed inline in strict mode:

  • { type: 'string' } - no constraints
  • { type: 'integer' } - no constraints
  • { type: 'boolean' } - no constraints
  • { type: 'array', items: { $ref: '...' } } - array of named schemas
  • { type: 'array', items: { type: 'string' } } - array of base types
  • $defs entries - the schema's own internal named types

Pass enableStrictGraph: false to restore permissive behaviour and downgrade errors to warnings.


Opting out

To disable strict enforcement entirely:

/**
 * Advanced Example 81 — opting out of strict graph mode
 * Demonstrates: strict enforcement is the default; pass enableStrictGraph: false
 * to downgrade inline-shape and duplicate errors to warnings.
 *
 * Both entry points accept the same flag: the JsonTology facade and the
 * SchemaRegistry it wraps.
 */

import { JsonTology } from '../../../src/index.js';
import { SchemaRegistry } from '../../../src/modules/registry/SchemaRegistry.js';

const SchemaWithInlineShape = {
  '$id': 'urn:example:Book',
  'properties': {
    'isbn': {
      'pattern': '^\\d{13}$',
      'type': 'string'
    }
  },
  'type': 'object'
} as const;

// Facade: relax enforcement for the whole instance. With strict mode off, the
// inline primitive shape registers with a warning instead of throwing.
const jt = JsonTology.create({
  'baseIri': 'https://example.com/',
  'enableStrictGraph': false,
  'schemas': [SchemaWithInlineShape]
});

// Registry directly: same flag, same effect.
const registry = new SchemaRegistry({ 'enableStrictGraph': false });

registry.set(SchemaWithInlineShape);

console.log('Permissive mode: inline shapes accepted on both entry points', jt.registry.size, registry.size);
Output
Press Execute to run this example against the real library.

With enableStrictGraph: false, inline shapes and duplicates emit logger.warn rather than throwing, unless the individual warning flags are also set to false.

Migrating existing schemas

If you have an existing codebase with inline shapes that cannot be refactored at once, start with strict mode off:

  1. Set enableStrictGraph: false and leave enableDuplicateDetection on (the default), so violations surface as warnings instead of throws.
  2. Run registry.findDuplicates() on your existing schema set to get the full list.
  3. For each duplicate, extract the shape to a named schema file and register it alongside its sibling schemas.
  4. Replace inline occurrences with { $ref: newSchema.$id }.
  5. Confirm all warnings are clear.
  6. Remove the enableStrictGraph: false override to restore the default enforcement.

CI script example

/**
 * CI script pattern — findDuplicates as a quality gate.
 *
 * Run `registry.findDuplicates()` after constructing the registry to detect
 * inline shapes that should be extracted to named schemas. In a CI script
 * the recipe is: log each duplicate, then exit non-zero if any were found.
 * This file shows the browser-safe core: no process.* calls.
 *
 * Demonstrates: `findDuplicates()` on the bookstore registry; the
 * shouldFail flag mirrors the CI gate result. A real CI wrapper would call
 * `process.exit(1)` on `shouldFail === true` — that Node-specific call lives
 * outside this example to keep the core browser-safe.
 */

import { bookstoreEntities } from '../bookstore/index.js';

const duplicates = bookstoreEntities.registry.findDuplicates();

const lines: string[] = [];

for (const dup of duplicates) {
  lines.push(`Duplicate: ${dup.schemaId}#${dup.pointer} ≡ ${dup.equivalentTo}`);
}

// shouldFail is the boolean CI gate result: true means a CI script should
// exit non-zero (e.g. `if (shouldFail) process.exit(1)` in a Node wrapper).
const shouldFail = duplicates.length > 0;

console.log(`findDuplicates: ${duplicates.length} duplicate(s) found`);
console.log(`Registry clean: ${!shouldFail}`);

if (lines.length > 0) {
  for (const line of lines) {
    console.log(line);
  }
}

console.assert(
  typeof shouldFail === 'boolean',
  'shouldFail is the boolean CI gate result'
);
console.assert(
  Array.isArray(lines),
  'lines is the array a CI script would print before exiting'
);
Output
Press Execute to run this example against the real library.

When inline is OK

Not every project needs strict graph mode. Inline shapes are fine when:

  • The schema has a single consumer and will never be reused.
  • It's a throwaway script or one-off data validation utility.
  • You're prototyping and the ontology contract isn't relevant yet.

The cost of inline shapes is borne only by graph users: OWL/SHACL output is less precise, findDuplicates() reports noise, and global type changes require manual find-and-replace. If you're not using the ontology output, inline shapes have no runtime cost.

Released under the MIT License.