Skip to content

JT keyword reference

Validation modes: Validation modes reference

json-tology adds a small set of jt:-prefixed keywords to JSON Schema. They are tracked alongside the standard keywords (see KNOWN_SCHEMA_KEYWORDS in src/constants/SCHEMA_KEYWORDS.ts) and read at specific points in the pipeline. Each keyword has a documented payload shape, semantics, and a clear "what reads it" answer.

KeywordPayloadModeRead by
jt:aliasstring | string[]RuntimeSchemaGraphSupport
jt:annotatedEdge(shape produced by Compose.annotatedEdge)RuntimeProjection (ABox triple-term emission)
jt:computedtrueRuntimeMaterializer (via computed-field map)
jt:config{ extra?, frozen?, strict? }RuntimeSchemaGraphSupport, Materializer
jt:frozentrueRuntimeMaterializer, SchemaRegistry
jt:restrictionsRestrictionRefType[]Compile-timeInferType (type narrowing), TBox projection (OWL)
jt:strictbooleanRuntimeSchemaGraphSupport (via enableStrictTypes)
x-jt-iriReftrueRuntimeProjection (ABox quad emission)
x-jt-languageBCP 47 language tag stringRuntimeProjection (ABox quad emission)
x-jt-predicateAbsolute IRI stringRuntimePredicateResolver, Projection

The bookstore schemas defined in the Bookstore Domain are used in the examples.


jt:alias

Payload. string or readonly string[].

Semantics. Records alternative IRIs (or local names) for the schema's owning class. Used when the same domain entity is known by more than one identifier - for example, a vendor IRI alongside the canonical project IRI.

Read by. normalizeAliases() in src/modules/graph/SchemaGraphSupport.ts. The aliases land on the canonical graph node and surface in OWL output as owl:equivalentClass or skos:altLabel declarations, depending on the active vocabulary plugins.

Use this when you publish RDF that needs to interoperate with externally minted IRIs for the same concept.

/**
 * jt:alias — Example 2: alternative IRIs for a schema class
 *
 * `jt:alias` records alternative IRIs for the schema's owning class.
 * When the bookstore domain publishes RDF, `BookSchema` carries an alias
 * pointing to the schema.org Book class so external consumers can map
 * the canonical IRI to the wider vocabulary.
 *
 * The alias surfaces as `owl:equivalentClass` or `skos:altLabel` in TBox
 * output, depending on active vocabulary plugins. At runtime, `jt:alias`
 * is only metadata — it has no effect on `validate` or `instantiate`.
 */

import {
  BookSchema, bookstoreEntities
} from '../bookstore/index.js';

// BookSchema carries jt:alias so the canonical graph node records
// alternative IRIs. The schema is otherwise a normal registered schema.
const errs = bookstoreEntities.validate(BookSchema.$id, {
  'authors': ['Michael Ende'],
  'inStock': true,
  'isbn': '9783522128001',
  'price': {
    'amount': 850,
    'currency': 'EUR'
  },
  'printStatus': 'outOfPrint',
  'publishedOn': '1979-09-01',
  'stockLevel': 5,
  'title': 'Die unendliche Geschichte'
});

// Validation is independent of alias metadata.
console.assert(errs.length === 0);

// The $id is always the canonical IRI — aliases do not change identity.
const canonicalId: string = BookSchema.$id;

console.assert(canonicalId === 'urn:bookstore:Book');

console.log('BookSchema.$id:', canonicalId);
console.log('Validation errors (expect 0):', errs.length);
console.log('jt:alias does not affect validation — validation errors are zero regardless of alias metadata.');
Output
Press Execute to run this example against the real library.

jt:computed

Payload. Literal true.

Semantics. Marks a property as derived. Callers cannot supply the value on input - doing so raises InstantiationError with code COMPUTED_INPUT_FORBIDDEN. The materializer fills it by calling the registered compute function during instantiate and materialize.

Read by. SchemaGraphSupport populates the graph node's computed field when jt:computed is present; Materializer then resolves the matching function from the registry's computedStore.

Use this when a property is mechanically derivable from other fields - an order total from line items, a displayName concatenating fields, a hash of canonical content. Pair it with addComputed (or the computeds constructor option) to register the function.

/**
 * jt:computed — Example 3: derived property registered via addComputed
 *
 * A property marked `jt:computed: true` is filled by the materializer
 * from a registered compute function rather than from caller-supplied
 * input. Supplying the field on input raises `InstantiationError` with
 * code `COMPUTED_INPUT_FORBIDDEN`.
 *
 * The canonical bookstore registers an order-total compute function on
 * `OrderSchema`. This example exercises that path: the `total` field is
 * omitted from raw input, and the materializer derives it from line items.
 *
 * See `examples/docs/computed/01-add-computed.ts` for the full
 * `addComputed` / `removeComputed` contract.
 */

import {
  aboxFixtures, bookstoreEntities, OrderSchema
} from '../bookstore/index.js';

// Valid order fixture — `orderTotal` is structurally present in the fixture,
// but the invariant verifies the total matches the computed sum.
const errs = bookstoreEntities.validate(OrderSchema.$id, aboxFixtures.order);

console.assert(errs.length === 0);

const totalAmount: number = aboxFixtures.order.orderTotal.amount;

console.assert(totalAmount === 850);

// OrderSchema.$id is a stable URN.
const schemaId: string = OrderSchema.$id;

console.assert(schemaId === 'urn:bookstore:Order');

console.log('OrderSchema.$id:', schemaId);
console.log('orderTotal.amount (jt:computed field, derived from orderLines):', totalAmount);
console.log('Validation errors (expect 0):', errs.length);
Output
Press Execute to run this example against the real library.

See addComputed for the function-side contract.

jt:config

Payload. Object with optional fields:

FieldTypeEffect
extra'allow' | 'forbid' | 'ignore'Policy for properties not declared in properties
frozenbooleanMaterializer returns a deeply frozen value
strictbooleanPer-schema toggle for strict-types behaviour

Semantics. A bundled, schema-local configuration block. The fields mirror standalone jt:frozen and jt:strict, plus an extra policy that has no standalone form. When both jt:frozen and jt:config.frozen are present, either being true is enough to freeze.

Read by. extractJtConfig() in src/modules/graph/SchemaGraphSupport.ts, with frozen consumed by Materializer and the freeze status also checked in SchemaRegistry.

Use this when you want to colocate several runtime policy bits without scattering individual keywords across the schema.

/**
 * jt:config — Example 4: bundled runtime policy (extra + frozen)
 *
 * `jt:config` co-locates several per-schema runtime policies. The
 * `extra` field controls how undeclared properties are handled during
 * instantiation; `frozen` deep-freezes the materialized value.
 *
 * This example registers a schema with `extra: 'forbid'` and verifies
 * that an unknown property causes `instantiate` to throw, while a clean
 * payload passes through normally.
 */

import { JsonTology } from '../../../src/index.js';

const AddressSchema = {
  '$id': 'urn:docs-schemas-04:Address',
  'jt:config': {
    // unknown properties → InstantiationError
    'extra': 'forbid',
    // result is deeply frozen
    'frozen': true
  },
  'properties': {
    'city': { 'type': 'string' },
    'postalCode': { 'type': 'string' },
    'street': { 'type': 'string' }
  },
  'required': [
    'street',
    'city',
    'postalCode'
  ],
  'type': 'object'
} as const;

// doc example with synthetic fixture schemas (strict-graph default does not throw because no inline duplicates)
const jt = JsonTology.create({
  'baseIri': 'urn:docs-schemas-04',
  'schemas': [AddressSchema] as const
});

// Clean payload — passes.
const address = jt.instantiate(AddressSchema.$id, {
  'city': 'München',
  'postalCode': '80331',
  'street': 'Reichenbachstraße 14'
});

console.assert(address.city === 'München');
console.assert(Object.isFrozen(address));

// Unknown property — throws because extra: 'forbid'.
let caughtExtra = false;

try {
  jt.instantiate(AddressSchema.$id, {
    'city': 'München',
    'postalCode': '80331',
    'street': 'Reichenbachstraße 14',
    'unknownField': 'should not be here'
  });
} catch {
  caughtExtra = true;
}

console.assert(caughtExtra);

console.log('instantiated address:', address);
console.log('address is frozen (jt:config.frozen: true):', Object.isFrozen(address));
console.log('unknown property rejected (jt:config.extra: forbid):', caughtExtra);
Output
Press Execute to run this example against the real library.

The three extra values:

  • 'allow' - unknown properties pass through unchanged.
  • 'forbid' - unknown properties trigger InstantiationError with code EXTRA_FORBIDDEN.
  • 'ignore' - unknown properties are silently stripped during instantiation.

jt:frozen

Payload. Literal true.

Semantics. Standalone shorthand for jt:config.frozen: true. The materializer applies a deep Object.freeze to the result. The schema registry also exposes the freeze flag on the canonical graph node so downstream consumers (validators, ontology projections) can reason about immutability.

Read by.

  • Materializer.isEffectivelyFrozen() in Materializer
  • SchemaGraphSupport populates jtFrozen on the graph node
  • SchemaRegistry checks freeze status during register/instantiate flow

Use this when every materialized value of this schema should be immutable - configuration objects, value objects, snapshot records.

/**
 * jt:frozen — Example 5: standalone freeze flag
 *
 * `jt:frozen: true` is shorthand for `jt:config: { frozen: true }`.
 * The materializer applies `Object.freeze` recursively to the result.
 * Use this when every materialized value of a schema must be immutable —
 * configuration objects, value objects, snapshot records.
 *
 * The canonical bookstore's `MoneySchema` carries `jt:frozen: true`.
 * This example demonstrates freeze via a focused one-shot registry so
 * the assertion is self-contained.
 */

import { JsonTology } from '../../../src/index.js';

const MoneySchema = {
  '$id': 'urn:docs-schemas-05:Money',
  'jt:frozen': true,
  'properties': {
    'amount': {
      'exclusiveMinimum': 0,
      'type': 'number'
    },
    'currency': {
      'maxLength': 3,
      'minLength': 3,
      'type': 'string'
    }
  },
  'required': [
    'amount',
    'currency'
  ],
  'type': 'object'
} as const;

const jt = JsonTology.create({
  'baseIri': 'urn:docs-schemas-05',
  // doc example with synthetic fixture schemas
  'enableStrictGraph': false,
  'schemas': [MoneySchema] as const
});

const price = jt.instantiate(MoneySchema.$id, {
  'amount': 850,
  'currency': 'EUR'
});

// Deep freeze applied by the materializer.
console.assert(Object.isFrozen(price));
console.assert(price.amount === 850);
console.assert(price.currency === 'EUR');

console.log('instantiated price:', price);
console.log('price is frozen (jt:frozen: true):', Object.isFrozen(price));
Output
Press Execute to run this example against the real library.

Prefer jt:config: { frozen: true } when you also need extra or strict. Use the standalone form when freeze is the only policy.

jt:strict

Payload. boolean.

Semantics. Per-schema override for strict-types behaviour - whether numeric strings coerce to numbers, whether null is rejected for typed fields, and so on. Without this keyword, the global enableStrictTypes option on JsonTology.create decides.

Read by. SchemaGraphSupport reads jt:strict and surfaces it on the graph node as jtStrict. The strict-types path consumes it during validation compilation.

Use this when one schema in a registry needs the opposite policy from the rest - for example, a wire-facing payload that must reject coercions even though the rest of the system allows them.

/**
 * jt:strict — Example 6: per-schema strict-types override
 *
 * `jt:strict: true` opts a single schema into strict-types mode even
 * when the global `enableStrictTypes` is `false`. A schema with this
 * keyword rejects string-to-number coercions and null for typed fields,
 * while other schemas in the same registry remain lenient.
 *
 * Use this on wire-facing payloads where silent coercion would mask
 * upstream bugs.
 */

import { JsonTology } from '../../../src/index.js';

const StrictIsbnSchema = {
  '$id': 'urn:docs-schemas-06:StrictIsbn',
  // reject coercions for this schema only
  'jt:strict': true,
  'properties': {
    'isbn': {
      'pattern': '^\\d{13}$',
      'type': 'string'
    },
    'price': {
      'exclusiveMinimum': 0,
      'type': 'number'
    },
    'title': { 'type': 'string' }
  },
  'required': [
    'isbn',
    'title',
    'price'
  ],
  'type': 'object'
} as const;

const jt = JsonTology.create({
  'baseIri': 'urn:docs-schemas-06',
  // doc example with synthetic fixture schemas
  'enableStrictGraph': false,
  // global: lenient
  'enableStrictTypes': false,
  'schemas': [StrictIsbnSchema] as const
});

// Correct types — passes.
const errsOk = jt.validate(StrictIsbnSchema.$id, {
  'isbn': '9783522128001',
  'price': 850,
  'title': 'Die unendliche Geschichte'
});

console.assert(errsOk.length === 0);

// String price on a jt:strict schema — fails even though global is lenient.
const errsStrict = jt.validate(StrictIsbnSchema.$id, {
  'isbn': '9783522128001',
  'price': '850',
  'title': 'Die unendliche Geschichte'
});

console.assert(errsStrict.length > 0);

console.log('correct types — validation errors (expect 0):', errsOk.length);
console.log('string price on jt:strict schema — validation errors (expect >0):', errsStrict.length);
console.log('jt:strict rejects string-to-number coercion per-schema, even when global enableStrictTypes is false.');
Output
Press Execute to run this example against the real library.

x-jt-predicate

Payload. Absolute IRI string.

Semantics. Pins the property to an explicit predicate IRI. toQuads uses this IRI as the predicate for the property's quad regardless of the registry baseIri, enableCanonicalPredicates, or predicateFor settings. It is the highest-precedence predicate binding after an absolute $id on the property schema.

Read by. PredicateResolver.resolve() in src/modules/graph/PredicateResolver.ts:53, step 1 in the five-step precedence chain.

Use this when a single property must align to an external vocabulary IRI (for example, a Schema.org predicate) without a registry-level predicateFor callback.

/**
 * Explicit per-property predicate via `x-jt-predicate`.
 *
 * Adding `x-jt-predicate: '<IRI>'` directly to a property schema pins
 * that property to a specific predicate IRI regardless of the registry
 * `baseIri`, `enableCanonicalPredicates`, or `predicateFor` settings.
 * It is the highest-precedence predicate binding — only the property
 * `$id` (when it is an absolute IRI) takes precedence over it.
 *
 * Use `x-jt-predicate` when a single property must align to an external
 * vocabulary IRI without touching the registry-level `predicateFor`
 * callback.
 */

import { JsonTology } from '../../../src/index.js';

// A minimal schema that pins `isbn` to the Schema.org ISBN predicate.
const BookSchema = {
  '$id': 'https://bookstore.example/Book',
  'properties': {
    'isbn': {
      '$ref': 'https://bookstore.example/Isbn',
      'x-jt-predicate': 'https://schema.org/isbn'
    },
    'title': { '$ref': 'https://bookstore.example/Title' }
  },
  'required': [
    'isbn',
    'title'
  ],
  'type': 'object'
} as const;

const IsbnSchema = {
  '$id': 'https://bookstore.example/Isbn',
  'type': 'string'
} as const;

const TitleSchema = {
  '$id': 'https://bookstore.example/Title',
  'type': 'string'
} as const;

const jt = JsonTology.create({
  'baseIri': 'https://bookstore.example',
  'enableStrictGraph': false,
  'schemas': [
    IsbnSchema,
    TitleSchema,
    BookSchema
  ] as const
});

const quads = jt.toQuads(BookSchema, {
  'isbn': '9783522128001',
  'title': 'Die unendliche Geschichte'
});

const predicates = quads.map((quad) => {
  return quad.predicate.value;
});

// isbn uses the pinned Schema.org predicate — not the flat canonical form.
console.assert(
  predicates.some((predicate) => {
    return predicate === 'https://schema.org/isbn';
  }),
  'isbn emitted as https://schema.org/isbn (x-jt-predicate)'
);

// title uses the default flat canonical form (no x-jt-predicate set).
console.assert(
  predicates.some((predicate) => {
    return predicate === 'https://bookstore.example/title';
  }),
  'title emitted as https://bookstore.example/title (default canonical)'
);

console.log('Predicates emitted:');
for (const predicate of [...new Set(predicates)].sort()) {
  console.log(' ', predicate);
}
Output
Press Execute to run this example against the real library.

See RDF predicates: priority order for the full precedence chain.

x-jt-iriRef

Payload. Literal true.

Semantics. Instructs toQuads to emit the string value as an RDF NamedNode rather than an xsd:string literal. Use it on string properties whose value is a dereferenceable IRI: URLs, URNs, or any identifier that should be treated as a node in the graph rather than a data value.

Read by. Projection reads propertySemantics.iriRef in src/modules/rdf/Projection.ts and routes the value through Terms.iri(value) instead of the literal path.

Use this when the property value is an IRI and you want it to participate in graph traversal as a named node rather than appear as a string literal.

/**
 * `x-jt-iriRef` (NamedNode) and `x-jt-language` (rdf:langString).
 *
 * Two bookstore schemas demonstrate how `toQuads` adjusts the RDF term
 * type for a string property based on schema-level annotations:
 *
 * - `DownloadUrl` carries `x-jt-iriRef: true` — the download URL is a
 *   dereferenceable resource, not merely a string value. `toQuads` emits
 *   it as a `NamedNode` (termType: 'NamedNode') rather than an xsd:string
 *   literal.
 *
 * - `Provenance` carries `x-jt-language: 'de'` — the chain-of-custody
 *   text for the Thienemann Verlag first edition is German prose. `toQuads`
 *   emits it as an `rdf:langString` literal tagged `@de`.
 *
 * Both fixtures are from the canonical bookstore ABox (Bastian Balthazar Bux
 * ordering a signed 1979 first edition of Die unendliche Geschichte).
 */

import { JsonTology } from '../../../src/index.js';
import {
  aboxFixtures,
  bookstoreSchemas,
  EBookSchema,
  SignedFirstEditionSchema
} from '../bookstore/index.js';

const jt = JsonTology.create({
  'baseIri': 'https://bookstore.example',
  'schemas': bookstoreSchemas
});

// ── x-jt-iriRef: true → NamedNode ─────────────────────────────────────────
const ebook = jt.instantiate(EBookSchema, aboxFixtures.ebook);
const ebookQuads = jt.toQuads(EBookSchema, ebook);

const downloadUrlQuad = ebookQuads.find((quad) => {
  return quad.predicate.value === 'https://bookstore.example/downloadUrl';
});

console.assert(downloadUrlQuad !== undefined, 'downloadUrl quad present');
console.assert(
  downloadUrlQuad?.object.termType === 'NamedNode',
  'downloadUrl emitted as NamedNode (x-jt-iriRef: true)'
);
console.assert(
  downloadUrlQuad?.object.value === aboxFixtures.ebook.downloadUrl,
  'NamedNode value matches the download URL'
);

console.log('downloadUrl quad:');
console.log('  predicate:', downloadUrlQuad?.predicate.value);
console.log('  object termType:', downloadUrlQuad?.object.termType);
console.log('  object value:', downloadUrlQuad?.object.value);

// ── x-jt-language: 'de' → rdf:langString @de ──────────────────────────────
const signedEd = jt.instantiate(SignedFirstEditionSchema, aboxFixtures.signedFirstEdition);
const signedQuads = jt.toQuads(SignedFirstEditionSchema, signedEd);

const provenanceQuad = signedQuads.find((quad) => {
  return quad.predicate.value === 'https://bookstore.example/provenance';
});

console.assert(provenanceQuad !== undefined, 'provenance quad present');
console.assert(
  provenanceQuad?.object.termType === 'Literal',
  'provenance emitted as Literal (rdf:langString)'
);

// Narrow to Literal to access .datatype and .language without casting.
const provObj = provenanceQuad?.object;
const provLiteral = provObj?.termType === 'Literal' ? provObj : undefined;

// rdf:langString datatype IRI per the RDF 1.1 specification.
console.assert(
  provLiteral?.datatype.value === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString',
  'provenance datatype is rdf:langString'
);

// Language tag is 'de' (x-jt-language: 'de' on ProvenanceSchema).
console.assert(
  provLiteral?.language === 'de',
  'provenance literal carries @de language tag'
);

console.log('\nprovenance quad:');
console.log('  predicate:', provenanceQuad?.predicate.value);
console.log('  object termType:', provenanceQuad?.object.termType);
console.log('  object datatype:', provLiteral?.datatype.value);
console.log('  object language:', provLiteral?.language);
console.log('  object value (first 60 chars):', provenanceQuad?.object.value.slice(0, 60));
Output
Press Execute to run this example against the real library.

The DownloadUrl schema in the bookstore domain (x-jt-iriRef: true) and the Provenance schema (x-jt-language: 'de') are both exercised in that example.

x-jt-language

Payload. BCP 47 language tag string (e.g. 'de', 'en', 'fr-BE').

Semantics. Tags the emitted string literal with the given language code, producing an rdf:langString rather than a plain xsd:string. The datatype is http://www.w3.org/1999/02/22-rdf-syntax-ns#langString and the language field on the Literal term carries the BCP 47 tag.

Read by. Projection reads propertySemantics.language in src/modules/rdf/Projection.ts and passes the tag to QuadFactory.literal(value, XSD.string, { language }).

Use this when the string property contains natural-language text in a known language (provenance descriptions, titles in a specific locale, editorial notes) and you want downstream reasoners or search engines to treat the language information as part of the triple.

The example above (example 103) also shows x-jt-language in action via the signedFirstEdition fixture's provenance field.

jt:annotatedEdge

Payload. The shape produced by Compose.annotatedEdge({ predicate, targetRef, annotations }). Do not write this keyword directly. Use the Compose.annotatedEdge builder, which produces the correct internal structure.

Semantics. Declares a property as an RDF-star annotated edge. toQuads emits two things for the property:

  1. A base triple: <subject> <edgePredicate> <targetIri>
  2. One annotation quad per declared annotation, whose subject is a triple-term (a Quad-subject quad per the RDF 1.2 / RDF-star specification): << subject edgePredicate targetIri >> <annotationPredicate> <value>

Both the base triple and all annotation quads share the same named graph. A graphIri option is required when calling toQuads; the default graph cannot carry triple-term quads.

Read by. Projection in src/modules/rdf/Projection.ts dispatches to projectAnnotatedEdge when the property structure kind is 'annotatedEdge'.

Use this when a relationship between two individuals carries metadata that belongs to the edge itself rather than to either endpoint: ratings on a review-to-book link, weights on a similarity edge, timestamps on a provenance arc.

/**
 * RDF-star annotated edge via `jt:annotatedEdge` / `Compose.annotatedEdge`.
 *
 * The bookstore `Review.reviewsBook` property uses an annotated edge: rather
 * than recording the rating only as a scalar on the Review, the annotation
 * attaches `ratingGiven` directly to the edge triple itself using the RDF 1.2
 * triple-term (RDF-star) encoding.
 *
 * `toQuads` emits:
 *   1. A base triple:  <review-IRI> <https://bookstore.example/reviews> <book-IRI>
 *   2. An annotation quad whose subject IS the base triple (triple-term):
 *      << <review-IRI> <https://bookstore.example/reviews> <book-IRI> >>
 *        <…Review#…#ratingGiven>  "5"^^xsd:integer
 *
 * Both quads share the same named graph. A named graph (`graphIri`) is
 * required — the default graph cannot carry triple-term quads.
 */

import type { QuadInterface } from '../../../src/interfaces/QuadInterface.js';
import {
  aboxFixtures,
  bookstoreEntities,
  ReviewSchema
} from '../bookstore/index.js';

const REVIEWS_GRAPH = 'https://bookstore.example/graph/reviews';

const review = bookstoreEntities.instantiate(ReviewSchema, aboxFixtures.reviewWithAnnotatedEdge);
const quads = bookstoreEntities.toQuads(ReviewSchema, review, { 'graphIri': REVIEWS_GRAPH });

// ── Base triple ────────────────────────────────────────────────────────────
const baseTriple = quads.find((quad) => {
  return quad.predicate.value === 'https://bookstore.example/reviews'
    && quad.subject.termType === 'NamedNode';
});

if (baseTriple === undefined) {
  throw new Error('expected base triple not found in quad set');
}

console.assert(
  baseTriple.object.termType === 'NamedNode',
  'base triple object is a NamedNode (book IRI)'
);
console.assert(
  baseTriple.object.value === aboxFixtures.reviewWithAnnotatedEdge.reviewsBook.target,
  'base triple object IRI matches fixture target'
);
console.assert(
  baseTriple.graph.value === REVIEWS_GRAPH,
  'base triple stamped with the named graph IRI'
);

// ── Triple-term annotation quad ────────────────────────────────────────────
// The annotation quad's subject is itself a Quad (the base triple).
type TripleTermQuad = QuadInterface & { 'subject': QuadInterface };

function isTripleTermQuad(quad: QuadInterface): quad is TripleTermQuad {
  return quad.subject.termType === 'Quad';
}

const annotationQuads = quads.filter(isTripleTermQuad);

console.assert(annotationQuads.length === 2, 'two annotation quads: ratingGiven + verifiedPurchase');

// Each annotation predicate is grounded to a schema.org term via `x-jt-predicate`
// on its annotation sub-schema, so annotations are addressable by vocabulary IRI.
const ratingAnnotation = annotationQuads.find((quad) => {
  return quad.predicate.value === 'https://schema.org/ratingValue';
});
const verifiedAnnotation = annotationQuads.find((quad) => {
  return quad.predicate.value === 'https://schema.org/verified';
});

if (ratingAnnotation === undefined || verifiedAnnotation === undefined) {
  throw new Error('expected grounded annotation quads not found in quad set');
}

console.assert(
  ratingAnnotation.object.value === String(aboxFixtures.reviewWithAnnotatedEdge.reviewsBook.annotations.ratingGiven),
  'ratingGiven value matches fixture'
);
console.assert(
  verifiedAnnotation.object.value === String(aboxFixtures.reviewWithAnnotatedEdge.reviewsBook.annotations.verifiedPurchase),
  'verifiedPurchase value matches fixture'
);
console.assert(
  ratingAnnotation.graph.value === REVIEWS_GRAPH,
  'annotation quad in the same named graph as the base triple'
);

console.log('Base triple:');
console.log('  subject:', baseTriple.subject.value);
console.log('  predicate:', baseTriple.predicate.value);
console.log('  object:', baseTriple.object.value);
console.log('  graph:', baseTriple.graph.value);

console.log('\nAnnotation quads (triple-term subject, predicates grounded to schema.org):');
for (const annotation of [
  ratingAnnotation,
  verifiedAnnotation
]) {
  console.log('  subject termType:', annotation.subject.termType);
  console.log('  predicate:', annotation.predicate.value);
  console.log('  object:', annotation.object.value);
  console.log('  graph:', annotation.graph.value);
}
Output
Press Execute to run this example against the real library.

Predicate grounding on annotation sub-schemas

Annotation predicates on an annotated edge can be grounded to a shared vocabulary IRI using x-jt-predicate (and optionally $id) on the annotation sub-schema — exactly like regular property sub-schemas. The bookstore Review edge grounds ratingGiven to https://schema.org/ratingValue and verifiedPurchase to https://schema.org/verified this way:

ts
Compose.annotatedEdge({
  predicate: 'https://bookstore.example/reviewed',
  targetRef: BookSchema.$id,
  annotations: {
    ratingGiven: {
      $id: 'https://schema.org/ratingValue',
      'x-jt-predicate': 'https://schema.org/ratingValue',
      type: 'number',
    },
    verifiedPurchase: {
      $id: 'https://schema.org/verified',
      'x-jt-predicate': 'https://schema.org/verified',
      type: 'boolean',
    },
  },
})

The x-jt-predicate value on the annotation sub-schema sets the quad predicate for that annotation triple. The $id value, when an absolute IRI, provides a fallback — but x-jt-predicate always wins when both are present (see the predicate priority table).

See RDF round-trip for toQuads / fromQuads documentation.

See also

Released under the MIT License.