Skip to content

rdf/turtle: JSON-LD @context array form not handled — locally-resolvable contexts only #389

@melvincarvalho

Description

@melvincarvalho

Surfaced during #388 (Phase A of #386).

Symptom

JSON-LD documents with @context in array form (e.g. [ "https://www.w3.org/ns/cid/v1", { ...inline prefixes... } ]) lose all term expansion when converted to Turtle via the conneg layer. Predicates appear as bare relative IRIs (<inbox>, <controller>, etc.) instead of expanding to their full namespace URIs.

Reproduction (during #388 development)

Switching src/webid/profile.js to emit @context as an array with https://www.w3.org/ns/cid/v1 first caused the existing #320 Turtle conneg test to fail. Output excerpt:

<http://...#me> a foaf:Person, schema:Person;
    foaf:name "webidturtletest";
    <inbox> "http://...inbox/";
    <storage> "http://...";
    <oidcIssuer> "http://...";
    ...
    <service> <http://...#oidc>.
<http://...#oidc> a <lws:OpenIdProvider>;
    <serviceEndpoint> "http://...".

Every term is a bare relative IRI. Should be ldp:inbox, pim:storage, solid:oidcIssuer, etc.

Root cause

src/rdf/turtle.js:182:

mergedContext = { ...mergedContext, ...doc['@context'] };

A shallow object spread. When doc['@context'] is an array, this copies array indices 0, 1, … as keys, breaking lookups. The function silently produces a context object where only the inline-object form survives — and only if it's a single object, not an array element.

Design constraint: don't fetch remote contexts

JSS deliberately doesn't dereference external context URLs at conversion time. Doing so would:

  • SSRF risk: any URL in any user-supplied JSON-LD becomes a fetch trigger from the server
  • Reliability: depend on third-party context hosts at request time
  • Latency: every Turtle conneg becomes an outbound HTTPS round-trip
  • Cache complexity: external contexts can change; do we cache, validate, refresh?

So the fix should not be "fetch the URL and merge it." Two reasonable fixes that preserve the no-remote-fetch invariant:

Option A — handle array form, ignore string entries

Iterate @context if it's an array; merge each entry that is an object, ignore each entry that is a string URL. Documents that need the URL-imported context's terms must declare those terms inline too (the pattern #388 ended up using).

function mergeContext(target, ctx) {
  if (!ctx) return target;
  if (Array.isArray(ctx)) {
    for (const entry of ctx) mergeContext(target, entry);
    return target;
  }
  if (typeof ctx === 'object') return Object.assign(target, ctx);
  // String URLs ignored — JSS doesn't fetch external contexts
  return target;
}

This makes JSS tolerate the array form (which is increasingly common — CID v1 docs, VC docs, DID docs all use it) without changing the no-fetch policy.

Option B — bundle well-known contexts as static files

Ship a small in-tree map of URL → static-context-object for the contexts JSS itself emits or expects (CID v1, AS2, DID core, schema.org subset). Reads cost zero — the file is just import ed at startup.

Pros: documents using @context: ["https://www.w3.org/ns/cid/v1", {…}] get the imported terms expanded correctly without any network. Solves both #388 and any future LWS / VC / DID work.

Cons: a small set of contexts that need maintenance when upstream changes. Mitigation: snapshot via test, alert on diff during periodic CI run.

Combined

Probably the right answer is A first, B opportunistically. A makes the array form non-crashing; B adds real support for canonical W3C contexts as we need them.

Acceptance

  • src/rdf/turtle.js accepts array @context without losing term expansion from inline objects in the same array
  • String entries in @context arrays are ignored gracefully (no fetch attempted)
  • Test: a JSON-LD document with @context: ["https://www.w3.org/ns/cid/v1", { foaf, … }] round-trips to Turtle with foaf:name etc. preserved
  • (Optional, separate PR) Bundled-context map for CID v1 + a couple of common LWS/W3C contexts

Refs

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions