Search by

accredifysg / php-json-ld

skydudie

A PHP implementation of the JSON-LD 1.1 specification.

Package info

github.com/Accredifysg/PHP-JSON-LD

Homepage

pkg:composer/accredifysg/php-json-ld

Statistics

Installs: 502

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 5

v2.2.0 2026-09-14 05:03 UTC

README

A PHP implementation of the JSON-LD 1.1 specification.

PHP-JSON-LD implements all six JSON-LD 1.1 processing algorithms — Expansion, Compaction, Flattening, Serialize to RDF (toRdf), RDF to JSON-LD (fromRdf), and Framing — validated against the official W3C test suites (1,287 of 1,302 tests passing, with Compaction and Flattening fully conformant) and cross-checked against reference implementations for identical canonical RDF output. The public API is stable and the project follows Semantic Versioning.

Highlights

  • Complete algorithm coverage of the JSON-LD 1.1 Processing Algorithms and API and JSON-LD 1.1 Framing.
  • Conformance gated in CI against the official W3C suites: any regression — or a documented blocker that silently starts passing — fails the build.
  • Cross-implementation interoperability: RDF output is verified against RDFC-1.0 canonical N-Quads goldens generated by jsonld.js over realistic verifiable-credential documents, so signatures computed over this library's output verify in other conformant processors (see tests/Interop).
  • Pluggable document loading: implement one DocumentLoader interface to serve known @context URLs from local resources — recommended for verifiable-credential pipelines — or use the bundled PSR-18 HTTP loader.
  • No mandatory HTTP dependency: bring your own PSR-18 client, or none.

Installation

composer require accredifysg/php-json-ld

Requires PHP 8.2+. The bundled HttpDocumentLoader needs a PSR-18 HTTP client and PSR-17 request factory (e.g. guzzlehttp/guzzle + guzzlehttp/psr7); alternatively, implement Accredify\JsonLd\Contracts\DocumentLoader yourself and no HTTP client is required.

Usage

use Accredify\JsonLd\JsonLdProcessor;
use Accredify\JsonLd\Loaders\CachingDocumentLoader;
use Accredify\JsonLd\Loaders\HttpDocumentLoader;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;

$loader = new CachingDocumentLoader(
    new HttpDocumentLoader(new Client, new HttpFactory),
);

$processor = new JsonLdProcessor($loader);

$expanded = $processor->expand($document);
$compacted = $processor->compact($expanded->toArray(), $context)->toArray();
$nQuads = $processor->toRdf($document)->toNQuads();

To serve known contexts from local files (recommended for verifiable credentials), implement Accredify\JsonLd\Contracts\DocumentLoader. See tests/Interop/Support/FixtureDocumentLoader.php for a compact example.

Framing note: a frame's {} wildcard cannot be represented in PHP's associative-array model (it decodes identically to []), so the framing API treats an empty frame value as match none and accepts the documented Expansion::FRAME_WILDCARD sentinel for a wildcard.

Safe mode

JSON-LD processing is lossy by design: an undefined term, a relative IRI, or a malformed value is silently dropped rather than reported. For documents that feed RDF canonicalization and signing, that is a security hole — W3C VC-DATA-INTEGRITY 1.0 §2.4.3 "Securing Data Losslessly" makes failing closed a normative requirement:

Implementations that use JSON-LD processing, such as RDF Dataset Canonicalization [RDF-CANON], MUST throw an error, which SHOULD be DATA_LOSS_DETECTION_ERROR, when data is dropped by a JSON-LD processor, such as when an undefined term is detected in an input document.

Pass safe: true (mirroring jsonld.js's safe option) and every drop site throws Accredify\JsonLd\Exceptions\DataLossException instead:

use Accredify\JsonLd\Exceptions\DataLossException;
use Accredify\JsonLd\JsonLdOptions;

try {
    $nQuads = $processor->toRdf($document, new JsonLdOptions(safe: true))->toNQuads();
} catch (DataLossException $e) {
    // e.g. "Safe mode: term 'alumniOf' does not expand to an absolute IRI
    //        or keyword (invalid property)"
    $e->eventCode; // 'invalid property' — jsonld.js-compatible event code
    $e->details;   // ['term' => 'alumniOf', …] — the dropped datum
}

The default (safe: false) is byte-identical to previous releases and to the spec-mandated lossy behaviour the W3C suites expect. Canonicalization/signing pipelines should always opt in: an empty-output check catches only the total-drop case, while safe mode also catches partial drops — an undefined credentialSubject claim that would otherwise sign, verify, and remain attacker-editable.

Event codes use jsonld.js's safe-mode catalog (lib/events.js) where an equivalent site exists:

invalid property (undefined / null-mapped / keyword-shaped terms), free-floating scalar, object with only @value, object with only @list, object with only @id, object with only @language, empty object, null @value value, reserved term, reserved @id value, reserved @reverse value, relative @vocab reference (document-level and scoped), relative @id reference, relative @type reference (at expansion and serialization), relative subject reference, relative predicate reference, relative object reference, relative graph reference, blank node predicate (suppressed by produceGeneralizedRdf: true), invalid @language value (malformed BCP47 tags — at expansion, toRdf, and fromRdf, like jsonld.js), rdfDirection not set.

Drop sites specific to this implementation carry their own codes: context load failed (a scoped context needs a DocumentLoader none is wired), invalid scoped term definition, invalid @direction value (a malformed @direction in hand-built expanded input handed directly to the serializer — the public pipeline rejects it at expansion as an unconditional error), invalid @id value (non-string @id relabelled as a blank node), invalid @value serialization (non-scalar @value coerced to ""), invalid @json serialization (NaN/Infinity in a @json literal), invalid map key (PHP decodes numeric-string JSON keys to integers, which jsonld.js would process), and dropped object (residual malformed shapes).

Not flagged, by design: null property/entry values (the spec's removal semantics, not data loss), a scoped '@language': null / '@direction': null reset (this processor's output for it is already correct), @index entries that carry no RDF statement (spec-correct), frame match patterns during frame() (wildcards and @value: null are queries, not data), and spec-mandated duplicate collapsing in the node map.

One deliberate divergence from jsonld.js: a genuine keyword used as a node entry where it is meaningless (@default / @vocab / a frame keyword outside a frame) throws invalid property here, because its value IS dropped from the expanded output — jsonld.js safe mode stays silent on those.

Conformance

The package is tested against the official W3C JSON-LD 1.1 test suite (git submodule at tests/w3c/; harness documented in tests/W3c/README.md).

Algorithm Spec W3C suite Passing Conformance
Expansion §5.5 385 381 4 documented blockers
Compaction §5.6 246 246 100%
Serialize to RDF (toRdf) §7 467 463 4 documented blockers
Flattening §4.6 58 58 100%
RDF to JSON-LD (fromRdf) §4.9 54 50 4 documented blockers
Framing framing §4 92 89 3 documented blockers

Totals: 1,287 / 1,302 passing. The 15 residual non-conformances are negative-test, non-normative, or environment limits, carried as an explicit expected-failure allowlist (tests/W3c/KnownBlockers.php) that gates CI in both directions — a new regression fails the build, and so does a listed blocker that starts passing:

  • #tc032 / #tc033unused embedded contexts aren't validated (negative tests)
  • #ter56 — redefining the @context keyword isn't rejected (negative test)
  • #t0122 (expand only) — keyword-shaped (@) IRIs are dropped rather than kept as {@id: null} (non-normative)
  • #tjs10 (toRdf only) — JSON-literal structural canonicalization differs (PHP cannot distinguish {} from [])
  • #t0008 / #tli03 (fromRdf) — list-of-lists conversion; single-level lists are fully supported
  • #tdi11 / #tdi12 (fromRdf, non-normative) — compound-literal direction folding
  • #t0010 (framing) — compaction safe-mode rejects dcterms:creator as an IRI confused with the dcterms prefix (the reference processor errors here too)
  • #t0045 (framing) — @language case-normalization (expansion preserves case, which the signature-critical toRdf bytes depend on)
  • #t0059 (framing) — the legacy @embed: @last mode (the @once default is implemented)
composer test        # unit tests (includes the interop corpus)
composer test:w3c    # the W3C conformance suites

Implementation report

A W3C EARL implementation report is generated for each release and submitted to the JSON-LD 1.1 implementation report. To regenerate it:

vendor/bin/pest --testsuite W3C --log-junit w3c-junit.xml
php scripts/generate-earl-report.php w3c-junit.xml <version> > reports/php-json-ld-earl.ttl

Tests marked specVersion: json-ld-1.0 are excluded from the report, matching the consolidated report's JSON-LD 1.1 scope.

Interoperability

The W3C suite alone cannot catch a processor that is consistently wrong: internal sign/verify round-trips recompute the same dataset on both sides, and a divergence only surfaces when another implementation checks the signature. tests/Interop closes that gap with realistic VC 2.0 and Open Badges v3 documents whose RDFC-1.0 canonical N-Quads — the exact bytes an eddsa-rdfc-2022 proof is computed over — are pinned to goldens generated by jsonld.js and compared as datasets on every CI run.

Development notes

tests/Algorithms/Characterization/ holds JSON snapshots of the expander's output over sample verifiable-credential documents. They are not a spec-conformance reference; they pin behaviour so any change to expansion output lands as a reviewable diff, to be paired with matching updates in downstream consumers (e.g. signed-credential fixtures).

License

MIT © Accredify