Skip to content
Advanced

4 JSON Schema Reference Checks Before Release

·9 min read
4 JSON Schema Reference Checks Before Release - Advanced guide from JSON Prettifier

Quick answer: Run four JSON Schema reference checks before release: confirm each resolved base URI, verify fragment targets, restrict resource retrieval, and test the final generated dependency graph with recursive fixtures. A schema can be valid JSON while pointing at missing definitions or relying on resources that exist only on a developer's machine.

The schema works by itself, then fails when a tool generator places it inside a larger document. The reference string has not changed. Its surrounding resource structure has, which is enough to change what the consumer can resolve.

An April 26, 2026 Spring AI report describes that class of nested-definition failure. The issue is closed, so it is a useful regression scenario, not evidence that every current release remains affected. This guide targets reference deployment rather than basic schema authoring; start with the JSON Schema primer if $defs and $ref are new to you.

1. Does each relative reference resolve against the intended base URI?

Resolve every representative relative reference against its actual schema base, then compare the resulting absolute identifier with the resource you intended to use. Include nested resource identifiers and path changes. A relative reference is not automatically relative to a repository folder, and moving a local file does not necessarily change its declared schema identity.

Write down the expected identifier before testing. A schema named order.json under one directory can refer to a sibling definition, while a trailing slash or an embedded identifier changes that relationship. A test that only checks whether something resolves can pass against the wrong definition if both candidate resources exist.

The JSON Schema structuring guide explains how identifiers establish resource scope. For a concrete implementation perspective, Sourcemeta's resolution guide distinguishes relative URIs from filesystem paths and describes initial retrieval locations. Read the selected validator's behavior rather than assuming every command-line tool supplies the same initial base.

This executable JavaScript fixture tests URI arithmetic only. It does not discover schema resources or implement every JSON Schema resolution rule:

import assert from 'node:assert/strict';
const root = 'https://schemas.example.test/contracts/order.json';
assert.equal(new URL('common.json', root).href,
  'https://schemas.example.test/contracts/common.json');
const nested = new URL('line/item.json', root).href;
assert.equal(new URL('../money.json', nested).href,
  'https://schemas.example.test/contracts/money.json');
assert.notEqual(new URL('common.json', root).href,
  new URL('common.json', root + '/').href);

MDN documents the URL constructor's relative-resolution behavior. The fixture makes a common path mistake visible without claiming that URL construction alone validates a schema. For your actual validator, assert both the selected resource identity and the outcome for a deliberately distinctive instance.

Use a minimal positive and negative pair: if the intended external definition accepts an integer but a similarly named definition accepts a string, test both types. That catches accidental resolution to a valid but incorrect target. Repeat after bundling, relocating assets, or changing the schema generator.

Keep identifiers stable according to a documented versioning policy. Reusing one identifier for incompatible definitions makes a successful lookup ambiguous operationally, even when the URI syntax is fine. Include resource names and safe version labels in build diagnostics so a release can show exactly which contract it loaded.

2. Do fragments select the right schema after definitions are packaged?

Test pointer fragments and named anchors against the final schema resource, including keys that contain a slash or tilde. Do not assume a reference follows a definition when a generator moves it. Packaging can leave a syntactically unchanged reference pointing at a location that no longer contains the intended schema.

A pointer identifies a location, while an anchor gives a named schema location within its resource. Those mechanisms are not interchangeable string conventions. A registry can find the correct document and still fail to locate the requested fragment. Report that as a target-resolution failure, not as invalid customer data.

RFC 6901 defines JSON Pointer token escaping. A slash within a property name is represented by ~1, and a tilde by ~0. URI fragment decoding is another layer; a general resolver must handle that layer too. The narrow helper below deliberately supports only simple, non-percent-encoded object-pointer fixtures:

import assert from 'node:assert/strict';
const schema = { $defs: { 'price/net~v1': { type: 'number' } } };
function fixturePointer(document, fragment) {
  assert(fragment.startsWith('#/'));
  let current = document;
  for (const part of fragment.slice(2).split('/')) {
    const key = part.replace(/~1/g, '/').replace(/~0/g, '~');
    assert(current !== null && typeof current === 'object');
    assert(Object.hasOwn(current, key), 'missing fixture target');
    current = current[key];
  }
  return current;
}
assert.equal(fixturePointer(schema, '#/$defs/price~1net~0v1').type, 'number');
assert.throws(() => fixturePointer(schema, '#/$defs/missing'));

Use JSON Viewer to inspect the generated tree and locate the relevant definitions. Its copied paths are JSONPath expressions, not automatically valid $ref fragments. The JSONPath versus JSON Pointer guide explains why those addresses should not be substituted for each other.

For named anchors, write actual validator integration tests rather than extending this helper with a recursive search for any matching text. Resource boundaries and dialect rules matter. An anchor with the right spelling in the wrong embedded resource is not necessarily the target you intended.

Keep a fixture where a generator wraps a component under a parameter property. Assert the target after that final step, not only before wrapping. If a transformation relocates definitions, require it to preserve reference meaning or reject the transformation. Blindly hoisting every definition can create name collisions between independently generated components.

3. Can validation access only the resources you explicitly approved?

Supply application-owned references through an explicit registry and test missing resources with network access unavailable. An identifier does not grant permission to fetch arbitrary content. If dynamic retrieval is necessary, define and test destination, size, timeout, redirect, and cache policies before allowing schema evaluation to initiate external access.

Python jsonschema's referencing documentation describes an explicit registry and configurable retrieval, warning about the security implications. Its current registry behavior is not the same as the older resolver's automatic retrieval behavior. Migration tests should make that difference visible rather than restoring permissive fetching merely to silence a missing-reference error.

The following fixture models a closed application registry. It is not an implementation of JSON Schema resource discovery, URI normalization, or anchor lookup:

import assert from 'node:assert/strict';
const resources = new Map();
function add(id, schema) {
  assert(!resources.has(id), 'duplicate registry identity');
  resources.set(id, JSON.stringify(schema));
}
function retrieve(id) {
  assert(resources.has(id), 'unapproved or unavailable resource');
  return JSON.parse(resources.get(id));
}
add('https://schemas.example.test/money', { type: 'number' });
assert.equal(retrieve('https://schemas.example.test/money').type, 'number');
assert.throws(() => retrieve('https://unapproved.example.test/schema'));
assert.throws(() => add('https://schemas.example.test/money', {}));

For background, a Uniform Resource Identifier identifies a resource. It does not establish your application's authorization policy. A URL-shaped schema name can be useful for identity without requiring a network request to that URL.

If your deployment needs retrieval, test it with a controlled fake loader first. Assert which requests are attempted, which are rejected, and whether redirects receive the same scrutiny as the original address. Keep real network credentials out of schema fixtures. A host allowlist by itself is not a complete defense if redirects, alternate schemes, or destination resolution can change where the request lands.

Bound both the fetched document and the total dependency graph. Many individually small schemas can still create substantial work. Preserve a release manifest of approved resource identities and content digests when reproducibility matters, and fail when a required resource is unavailable. Do not substitute an empty schema as an emergency fallback: that can convert infrastructure failure into unintended acceptance.

Separate resource errors from instance-validation errors in application handling. A missing dependency usually means the service cannot establish its contract, not that a customer should edit a field. Return an appropriate service failure and retain safe operational evidence instead of displaying a misleading property-level validation message.

Exercise the failure path deliberately. Start the test with the required resource removed from the registry, submit the same otherwise valid instance, and assert that the application does not continue into its normal persistence path. Then restore the resource and verify recovery. A resolver test that only expects an exception can miss an adapter that catches the exception and mistakenly returns a successful validation result.

4. Does the final recursive schema behave correctly in every consumer?

Test the final packaged schema with finite recursive instances, negative leaf values, and the exact consumer dialect and options. Verify that wrapping, bundling, and caching preserve resource identities and reference targets. A recursive schema is not a circular runtime object, and successful JSON serialization alone does not prove that evaluation terminates or enforces the intended rules.

Ajv's combining-schemas guide documents recursive references and preloaded dependencies. Its schema-management guidance discusses compilation and reuse. Keep compilation and validation behavior observable separately; repeatedly rebuilding a validator during ordinary requests can hide deployment problems and add unnecessary work.

This structural fixture captures a wrapping regression without pretending to be a validator:

import assert from 'node:assert/strict';
const component = {
  $defs: { node: { type: 'object', properties: {
    child: { $ref: '#/$defs/node' }
  } } },
  $ref: '#/$defs/node'
};
const wrapped = { type: 'object', properties: { request: component } };
assert.equal(Object.hasOwn(wrapped, '$defs'), false);
assert.equal(wrapped.properties.request.$ref, '#/$defs/node');
const correctedFixture = {
  $defs: structuredClone(component.$defs),
  type: 'object', properties: { request: { $ref: '#/$defs/node' } }
};
assert(correctedFixture.$defs.node);
assert.equal(correctedFixture.properties.request.$ref, '#/$defs/node');

The corrected fixture only demonstrates the expected placement for this simple case. It is not a general bundler: embedded identifiers, anchors, duplicate definition names, and dynamic references need dialect-aware treatment. The 2020-12 core specification is the technical reference for that dialect; do not infer production support from an unreleased specification branch.

Build an integration matrix with the actual generator and consumers. Include a shallow valid tree, a deeper valid tree within the application's limits, an invalid leaf, and a missing referenced resource. Confirm the negative instance is rejected because the intended leaf rule was evaluated, not because the consumer failed to understand the schema entirely.

Ajv's security guidance distinguishes recursive schema references from circular JavaScript objects and warns about untrusted-schema resource costs. Apply suitable size and depth limits and use isolated tests for potentially expensive cases. No universal safe recursion depth follows from the fact that a library supports recursive schemas.

Finally, repeat the checks against the release artifact in a clean environment. A warm developer cache can supply a dependency omitted from the package. A meaningful offline release test starts with the intended registry and proves the exported contract stands on its own. Keep the general schema validation checks alongside these reference-specific fixtures.

Test generated components with colliding definition names as a separate packaging case. Two independent parameters can each define a local address with different required fields. A naive merger that keeps the first definition can leave both references resolvable while enforcing the wrong contract for the second parameter. Assert distinctive positive and negative payloads for both components, not only the number of definitions in the output. A packaging change is successful when meaning survives, not merely when every reference string finds some object.

Keep the regression evidence compact enough to review. Store the generator input, final schema artifact, registry manifest, validator configuration, and expected instance outcomes together. When a dependency upgrade changes the artifact, review the changed targets and rerun the consumer tests before accepting the new output. This makes a closed historical issue useful over time: its failure shape stays covered even when the original package version is no longer deployed.

All four JavaScript examples were executed locally. They test URI, pointer, registry, and packaging invariants; no full schema-validator integration, external retrieval, or adversarial recursion test is claimed. Run those implementation-specific checks before treating your own reference graph as release-ready.

Frequently Asked Questions

Does a JSON Schema $id have to be a downloadable URL?
No. The identifier establishes schema identity and can establish a base for relative references; it does not require a server to host that schema at the same address. Supply the intended resources through your validator's registry or documented loading mechanism, and test resolution without assuming an HTTP request will succeed.
Does a local $ref resolve from the containing object?
Not simply because that object contains the reference. Resolution depends on the current schema resource and its base identifier. A fragment pointer can therefore target a different location than a developer expects after wrapping or embedding a schema. Test the final resource boundaries rather than interpreting the reference as an object-relative path.
Are recursive JSON Schema references the same as circular JavaScript objects?
No. A schema can describe recursive data using reference strings while remaining an ordinary serializable JSON document. A circular JavaScript object contains runtime identity links that JSON cannot directly represent. Test valid recursive schemas with finite instances, and review accidental reference loops that do not advance through instance structure separately.
Should a validator automatically download every referenced schema?
Not by default. Reference identifiers are not an instruction to grant arbitrary network or filesystem access. Prefer an explicit resource registry for application-owned schemas. If retrieval is required, define allowed destinations, limits, caching, and failure behavior, then verify that untrusted references cannot bypass those controls during validation.
Can JSON Viewer verify that my references resolve?
JSON Viewer can display the schema tree and help locate definitions or reference strings. It does not build a dialect-aware schema registry or prove that references resolve in your validator. Use the viewer for structural inspection, then run resolution and validation tests through the same engine and options used by your application.

Inspect the final schema tree

Explore definitions and reference strings in your generated schema, then verify resolution with your application validator.

Open JSON Viewer