Skip to content

Default @direction is not inherited across context layers — _cloneActiveContext omits @direction #586

Description

@skydudie

Summary

The active-context clone used by context processing copies @base, @language, and @vocab, but not @direction. As a result the default base direction is silently dropped whenever a new context layer is processed on top of an existing active context — a second document-level @context layer, a property-scoped context, a type-scoped context, an embedded node @context, or a remote context. The default language is inherited correctly in all of these cases; only @direction is lost.

Reproduced on jsonld.js 9.0.0; the omission is still present in _cloneActiveContext on current main.

Reproduction

import jsonld from 'jsonld';

// 1. Control: a single context layer sets both defaults — both apply.
console.log(JSON.stringify(await jsonld.expand({
  '@context': {'@language': 'en', '@direction': 'rtl'},
  'http://ex/p': 'v'
})));

// 2. A second document-level context layer: @language survives, @direction is lost.
console.log(JSON.stringify(await jsonld.expand({
  '@context': [{'@language': 'en', '@direction': 'rtl'}, {'dummy': 'http://ex/d'}],
  'http://ex/p': 'v'
})));

// 3. A property-scoped context: @language is inherited into the scope, @direction is lost.
console.log(JSON.stringify(await jsonld.expand({
  '@context': {
    '@language': 'en',
    '@direction': 'rtl',
    'thing': {'@id': 'http://ex/thing', '@context': {'other': 'http://ex/other'}}
  },
  'thing': {'http://ex/label': 'hello'}
})));

// 4. toRDF with rdfDirection (jsonld-signatures' default): the divergence reaches N-Quads.
console.log(await jsonld.toRDF({
  '@context': {
    '@language': 'en',
    '@direction': 'rtl',
    'thing': {'@id': 'http://ex/thing', '@context': {'other': 'http://ex/other'}}
  },
  '@id': 'http://ex/x',
  'http://ex/out': 'outside',
  'thing': {'http://ex/label': 'hello'}
}, {format: 'application/n-quads', rdfDirection: 'i18n-datatype'}));

Actual output (jsonld.js 9.0.0):

[{"http://ex/p":[{"@language":"en","@direction":"rtl","@value":"v"}]}]
[{"http://ex/p":[{"@language":"en","@value":"v"}]}]
[{"http://ex/thing":[{"http://ex/label":[{"@language":"en","@value":"hello"}]}]}]
<http://ex/x> <http://ex/out> "outside"^^<https://www.w3.org/ns/i18n#en_rtl> .
<http://ex/x> <http://ex/thing> _:b0 .
_:b0 <http://ex/label> "hello"@en .

Expected (per spec): case 2 keeps "@direction":"rtl"; case 3's hello carries "@direction":"rtl"; case 4 emits "hello"^^<https://www.w3.org/ns/i18n#en_rtl> for the in-scope literal. Note case 4: within one document, the top-level string gets the i18n datatype while the string inside the scope silently degrades to a plain language-tagged literal.

Root cause

_cloneActiveContext in lib/context.js:

if('@base' in this) {
  child['@base'] = this['@base'];
}
if('@language' in this) {
  child['@language'] = this['@language'];
}
if('@vocab' in this) {
  child['@vocab'] = this['@vocab'];
}

@direction is never copied. Context processing starts each layer with rval = rval.clone(), so any layer processed after the one that set @direction loses it (unless that layer restates it — explicitly setting @direction works, because the '@direction' in ctx handler assigns onto the clone; only inheritance is broken). The clone predates JSON-LD 1.1's base-direction support and appears never to have been updated.

Spec reference

JSON-LD 1.1 API, Context Processing: the active context "consists of: the active term definitions …, the current base IRI, an inverse context, an optional vocabulary mapping, an optional default language, an optional default base direction, and an optional previous context", and step 1 of the algorithm is "Initialize result to the result of cloning active context, with inverse context set to null." Step 5.8 then only modifies the default base direction "If context has a @direction entry" — an absent entry leaves the inherited value in place.

A one-line fix in _cloneActiveContext appears sufficient:

if('@direction' in this) {
  child['@direction'] = this['@direction'];
}

Notes

  • PyLD shares the identical omission_clone_active_context in lib/pyld/jsonld.py copies mappings, @base, previousContext, @language, @vocab and not @direction — so a fix here should probably be mirrored there.
  • Implementations diverge on this today: Titanium JSON-LD (Java) copies both defaultLanguage and defaultBaseDirection in its ActiveContext copy constructor, so it inherits @direction per spec and produces different expansion/N-Quads output than jsonld.js for the documents above.
  • The W3C test suite doesn't cover this — there is no json-ld-api fixture combining a default @direction with a subsequent context layer or scoped context, which is presumably why it has gone unnoticed. A suite test may be worth proposing alongside a fix.
  • Compatibility note: because jsonld-signatures defaults rdfDirection to i18n-datatype, fixing this changes canonical N-Quads (and therefore RDFC hashes / proof values) for any signed document that combines a default @direction with multiple context layers or scoped contexts. Related standards discussion on @direction/rdfDirection divergence in Data Integrity: RDFC-based cryptosuites do not specify rdfDirection; implementations already diverge on documents with @direction w3c/vc-data-integrity#366.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions