league/json-reference
A PHP library for resolving JSON References, including JSON Pointers ($ref) in JSON documents and schemas. It dereferences local and remote references through a simple API, with support for custom retrievers and integration with JSON Schema tooling.
league/json-reference is a small, single-purpose library: it parses JSON Reference strings (URI#/json/pointer), implements RFC 6901 JSON Pointers, and can dereference a document — walk every $ref and inline the target value.
Install (expect friction — see Gotchas):
composer require league/json-reference
Where to look first. The package is tiny; read these four files in order and you'll understand the whole thing in ~15 minutes:
| File | Role |
|---|---|
src/Reference.php |
Parses scheme://host/path#/pointer into URI + fragment |
src/Pointer.php |
RFC 6901 pointer evaluation against a document |
src/Loader/*.php |
Fetch a document by URI (FileLoader, CurlLoader, chain) |
src/Dereferencer.php |
Recursively replaces $ref nodes with resolved values |
First use case — inline a schema's internal refs. This is what 90% of consumers actually want: take a JSON Schema that uses $ref/definitions and produce one self-contained document you can hand to a validator or a code generator.
use League\JsonReference\Dereferencer;
$dereferencer = Dereferencer::dereferencer(); // pre-wired default loader chain
$schema = json_decode(file_get_contents('schema.json'));
$resolved = $dereferencer->dereference($schema);
file_put_contents('schema.bundled.json', json_encode($resolved, JSON_PRETTY_PRINT));
If that one-liner works on your schema, you're done — do not build anything else around this package.
1. Bundle-on-build, validate-at-runtime. Dereferencing is relatively expensive (recursive traversal + I/O per remote document). Do it once in a build/CI step and commit the bundled schema, rather than dereferencing per request:
// bin/bundle-schema.php
$dereferencer = Dereferencer::dereferencer();
echo json_encode($dereferencer->dereference(json_decode($json)), JSON_PRETTY_PRINT);
2. Custom loader for non-file documents. The extension point that matters most is LoaderInterface. Implement it to serve schemas from a database, an S3 key, or an in-memory fixture map — this also eliminates network calls in tests:
use League\JsonReference\Loader\LoaderInterface;
final class InMemoryLoader implements LoaderInterface
{
public function __construct(private array $docs) {}
public function load($uri)
{
$uri = (string) $uri;
if (!array_key_exists($uri, $this->docs)) {
throw new \RuntimeException("No document registered for {$uri}");
}
return json_decode($this->docs[$uri]);
}
}
$dereferencer = Dereferencer::dereferencer();
$dereferencer->registerLoader(new InMemoryLoader([
'https://example.com/user.json' => '{"type":"object","properties":{"id":{"type":"string"}}}',
]));
3. Lazy resolution instead of full dereference. If you only need one branch, skip Dereferencer and evaluate a pointer directly — no traversal, no cycle risk:
use League\JsonReference\Pointer;
$pointer = new Pointer('/definitions/user/properties/id');
$node = $pointer->get($document);
4. Parse references without resolving them. Useful for tooling — linters, ref-graph builders, "which files does this schema depend on?" reports:
use League\JsonReference\Reference;
$ref = Reference::create('https://example.com/schemas/user.json#/definitions/id');
$ref->getUri(); // https://example.com/schemas/user.json
// fragment / pointer accessors give you the JSON Pointer portion
5. Wrap the loader chain with a cache. Because loaders are just objects, a decorator that memoizes load($uri) per request is a five-line class and removes repeated fetches when the same remote schema is referenced from many places.
6. Pair with a validator, not instead of one. This library does not validate. The historical companion is league/json-guard; if you're starting fresh, prefer a validator that resolves $ref internally (opis/json-schema, justinrainbow/json-schema) and drop this dependency entirely.
opis/json-schema or league/uri plus your own pointer walk.$refs. Dereferencer inlines values; a self-referential schema (#/definitions/node referring to itself) either throws or recurses depending on the resolver's cycle handling. Test your schema specifically for recursion before shipping a bundling step — a naive json_encode of the result will also blow up on any surviving object cycle.$ref of user.json#/foo resolves against the containing document's URI, not the process CWD. If you pass a decoded object to dereference() with no notion of origin, relative refs cannot resolve correctly — feed absolute URIs, or register the document under its real URI in your loader.json_decode returns stdClass by default. Pointer traversal and the dereferencer's rewriting logic expect objects; passing associative arrays can silently break $ref replacement because the code looks for ->{'$ref'}. json_decode($json) (no true), matching the library's own tests, is the safe default.#/definitions/foo~1bar — ~1 means / and ~0 means ~ in a pointer token. Do not URL-decode the fragment before handing it to Pointer; percent-decoding and pointer-unescaping are two different steps and libraries get this order wrong.CurlLoader may attempt a real network request for a scheme you intended to intercept.Reference::create(...)->getUri() encountered before dereferencing. Nine times out of ten the failure is "wrong resolved URI," not "wrong pointer." Reading tests/ in the repo is more accurate than the README for exact method signatures — the docs are thin and a few methods were renamed across versions.LoaderInterface and the pointer/reference value objects. There is no plugin system for custom $ref semantics, no $id/$anchor scoping (JSON Schema draft-06+ behaviour), and no dialect awareness. If your schemas rely on $id base-URI rebasing, this library will resolve refs against the wrong base and you should not use it.How can I help you explore Laravel packages today?