Skip to content

RDF round-trip with toQuads / fromQuads

You only need this section if you want to project typed instance data out as RDF quads, run reasoning or graph queries over it, and then lift the resulting quads back into typed JS objects. This is unique to json-tology - no other validator on the comparison list offers a symmetric ABox round-trip.

The same canonical graph used for validation drives both directions. toQuads lowers a typed value into ABox quads; fromQuads lifts quads back through instantiate, applying defaults, transforms, and validation on the way in.

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


Quad / SubjectGroup

A Quad is the atomic unit of RDF data produced and consumed by toQuads / fromQuads. Each quad is a plain object with four fields:

  • subject: the IRI or blank-node identifier of the resource being described
  • predicate: the property IRI
  • object: the value (IRI, blank node, or typed literal)
  • graph: the named-graph IRI, or the default graph when omitted

A SubjectGroup is a convenience wrapper that groups all quads sharing the same subject, making it easier to reconstruct a single typed object from a quad array. fromQuads uses subject groups internally when lifting quads back into typed JS values via instantiate.


jt.toQuads

Declaration. Projects instance data through the canonical graph and returns a QuadInterface[] array of the projected ABox quads. The first argument is a schema object with $id (registers the schema if it is not already registered). The second argument is the typed value, normally the output of instantiate(). To serialize the quads as JSON-LD, pass them to an OntologyBuilder via addFromQuads(quads) and call jsonLd(), jsonLdObject(), or shaclObject() on the builder.

Use this when you want to publish a validated value as Linked Data, push it into a triple store, hand it to an OWL or SHACL reasoner, or merge it into a knowledge graph that already contains the matching TBox.

Don't use this when you only need a wire-form payload (use dump). Don't use it as a generic "to JSON" helper - quads carry RDF semantics, not display formatting.

Examples

Example 1: Project an order to ABox quads

/**
 * Project an order to ABox quads.
 *
 * toQuads validates the order against OrderSchema, then projects each
 * field through the canonical graph and returns QuadInterface[]. The
 * resulting quads can be passed to an OntologyBuilder via addFromQuads to
 * produce JSON-LD output.
 */

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

const order = bookstoreEntities.instantiate(OrderSchema, aboxFixtures.order);
const quads = bookstoreEntities.toQuads(OrderSchema, order);

console.assert(quads.length > 0, 'order projected to RDF quads');

const first = quads[0];

if (first === undefined) {
  throw new Error('expected at least one quad');
}

console.assert(typeof first.subject.value === 'string', 'first quad carries subject term');
console.assert(typeof first.predicate.value === 'string', 'first quad carries predicate term');

// For richer output (JSON-LD, SHACL composition) pass the quads through
// the ontology builder:
const ontology = bookstoreEntities.ontology().addFromQuads(quads);
const jsonLd = ontology.jsonLd();
const jsonLdObject = ontology.jsonLdObject();

console.assert(typeof jsonLd === 'string', 'JSON-LD string emitted');
console.assert(typeof jsonLdObject === 'object', 'JSON-LD object emitted');

console.log('order quad count:', quads.length);
console.log('first quad subject:', first.subject.value);
Output
Press Execute to run this example against the real library.

Example 2: Merge ABox with TBox in a single document

/**
 * Merge an order ABox with the bookstore TBox.
 *
 * toTbox() returns class and property declarations; toQuads(OrderSchema, order)
 * returns ABox individual assertions. Spreading both into a single @graph
 * produces a self-contained JSON-LD document with vocabulary and instance
 * data co-located.
 */

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

const order = bookstoreEntities.instantiate(OrderSchema, aboxFixtures.order);

const tbox = bookstoreEntities.toTbox();
const abox = bookstoreEntities.toQuads(OrderSchema, order);

// tbox graph — class and property declarations via JSON-LD formatter.
// abox — individual assertions (QuadInterface[]).
const tboxGraph = tbox.jsonLdObject()['@graph'] as unknown[];
const merged = {
  '@context': tbox.context(),
  '@graph': [
    ...tboxGraph,
    ...abox
  ]
};

console.assert(Boolean(merged['@context']), 'context present');
console.assert(merged['@graph'].length > tboxGraph.length, 'ABox extended TBox @graph');

console.log('TBox @graph nodes:', tboxGraph.length);
console.log('merged @graph nodes (TBox + ABox):', merged['@graph'].length);
Output
Press Execute to run this example against the real library.

Subject minting with iriFor

The default minter assigns <baseIri>/instances/<classId>-<contentHash> to every projected object. To override that, pass iriFor to toQuads:

/**
 * Subject minting strategies via the iriFor option on toQuads.
 *
 * Demonstrates each of the four minting forms accepted by toQuads:
 *   1. Fixed root-only override (string IRI).
 *   2. Anonymous blank-node subjects (the 'blank-node' literal).
 *   3. Skolemize.fromProperty — mint from a value field.
 *   4. Skolemize.wellKnownGenid — W3C RDF 1.1 §3.5 reversible pattern.
 *   5. Custom function — receives { path, value, depth }.
 */

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

const order = bookstoreEntities.instantiate(OrderSchema, aboxFixtures.order);
const customer = bookstoreEntities.instantiate(CustomerSchema, aboxFixtures.customer);

// 1. Root-only override (depth 0 wins; nested objects fall through):
const rootOverride = bookstoreEntities.toQuads(OrderSchema, order, { 'iriFor': 'https://shop.example.com/orders/A-1234' });

console.assert(rootOverride.length > 0, 'root override emitted quads');

// 2. Anonymous blank-node subjects for every emitted object:
const blankNodes = bookstoreEntities.toQuads(OrderSchema, order, { 'iriFor': 'blank-node' });

console.assert(blankNodes.length > 0, 'blank-node strategy emitted quads');

// 3. Mint from a property of the value (Customer has an id field):
const fromCustomerId = bookstoreEntities.toQuads(CustomerSchema, customer, { 'iriFor': Skolemize.fromProperty('customerId', { 'baseIri': 'https://shop.example.com/customers' }) });

console.assert(fromCustomerId.length > 0, 'fromProperty strategy emitted quads');

// 4. W3C RDF 1.1 §3.5 well-known genid pattern (reversible by deskolemize):
const genid = bookstoreEntities.toQuads(OrderSchema, order, { 'iriFor': Skolemize.wellKnownGenid('https://shop.example.com') });

console.assert(genid.length > 0, 'wellKnownGenid strategy emitted quads');

// 5. Custom function: receives { path, value, depth }; return undefined to fall through.
const custom = bookstoreEntities.toQuads(OrderSchema, order, {
  'iriFor': (ctx) => {
    return ctx.depth === 0
      ? `https://shop.example.com/orders/${(ctx.value as { 'orderId': string }).orderId}`
      : undefined;
  }
});

console.assert(custom.length > 0, 'custom function strategy emitted quads');

console.log('root override subject:', rootOverride[0]?.subject.value);
console.log('blank-node subject:', blankNodes[0]?.subject.value);
console.log('fromProperty (customerId) subject:', fromCustomerId[0]?.subject.value);
console.log('wellKnownGenid subject:', genid[0]?.subject.value);
console.log('custom fn subject:', custom[0]?.subject.value);
Output
Press Execute to run this example against the real library.

See skolemization for the strategy reference.

Blank-node subjects with BLANK_NODE_IRI_FOR

Pass { iriFor: BLANK_NODE_IRI_FOR } to produce anonymous blank-node subjects instead of IRI-named nodes. Useful for transient quads (e.g. SHACL validation input) where no persistent identity is needed.

/**
 * Advanced Example 112 — BLANK_NODE_IRI_FOR: anonymous blank-node subjects
 *
 * By default `toQuads()` mints well-known genid IRIs
 * (`https://…/.well-known/genid/…`) as subjects for every projected object.
 * Passing `{ iriFor: BLANK_NODE_IRI_FOR }` replaces those IRIs with
 * anonymous blank nodes (`_:b0`, `_:b1`, …) so no persistent identity is
 * embedded in the quad set.
 *
 * This is useful when the quads are transient (e.g. SHACL validation input)
 * and no persistent IRI identity is needed or desired.
 *
 * `BLANK_NODE_IRI_FOR` is the exported string constant `'blank-node'`.
 * Passing it as `iriFor` activates the blank-node skolemization strategy
 * inside `toQuads`.
 */

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

// ── Schema ────────────────────────────────────────────────────────────────

const AddressSchema = {
  '$id': 'https://bookstore.example/Address',
  'properties': {
    'city': { 'type': 'string' },
    'country': { 'type': 'string' }
  },
  'required': [
    'city',
    'country'
  ],
  'type': 'object'
} as const;

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

const instance = {
  'city': 'Berlin',
  'country': 'DE'
};

// ── Default: genid NamedNode subjects ─────────────────────────────────────
const defaultQuads = jt.toQuads(AddressSchema, instance);
const defaultSubject = defaultQuads.at(0)?.subject;

console.assert(
  defaultSubject?.termType === 'NamedNode',
  'default: subject is a NamedNode (well-known genid IRI)'
);
console.log('Default subject termType:', defaultSubject?.termType);
console.log('Default subject value:   ', defaultSubject?.value);

// ── Blank-node strategy ────────────────────────────────────────────────────
const blankQuads = jt.toQuads(AddressSchema, instance, { 'iriFor': BLANK_NODE_IRI_FOR });

// Every subject in the blank-node quad set is a BlankNode.
const nonBlankSubjects = blankQuads.filter((quad) => {
  return quad.subject.termType !== 'BlankNode';
});

console.assert(
  nonBlankSubjects.length === 0,
  'blank-node: all subjects are BlankNode terms'
);
console.assert(
  blankQuads.length > 0,
  'blank-node: quads were emitted'
);

const blankSubject = blankQuads.at(0)?.subject;

console.log('\nBlank-node subject termType:', blankSubject?.termType);
console.log('Blank-node subject value:   ', blankSubject?.value);

console.log('\nBlank-node quad set:');
for (const quad of blankQuads) {
  console.log(
    ' ',
    quad.subject.value,
    quad.predicate.value,
    quad.object.value
  );
}

// ── BLANK_NODE_IRI_FOR constant value ─────────────────────────────────────
// The constant is the literal string 'blank-node'. Log it so the value is
// visible in the example output — the tautological compile-time assertion
// is omitted per the no-unnecessary-condition rule.
console.log('\nBLANK_NODE_IRI_FOR constant:', BLANK_NODE_IRI_FOR);
Output
Press Execute to run this example against the real library.

Graph IRI

Set the graph field on every emitted quad with graphIri:

/**
 * Set the named graph IRI for every emitted quad via the graphIri option.
 *
 * Useful for partitioning quads into named graphs (e.g. monthly slices, or
 * a per-tenant graph) without rewriting subjects.
 */

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

const order = bookstoreEntities.instantiate(OrderSchema, aboxFixtures.order);

const quads = bookstoreEntities.toQuads(OrderSchema, order, { 'graphIri': 'https://shop.example.com/graphs/2026-01' });

console.assert(quads.length > 0, 'quads emitted');
console.assert(
  quads.every((quad) => {
    return quad.graph.value === 'https://shop.example.com/graphs/2026-01';
  }),
  'every quad carries the configured named graph'
);

console.log('quad count:', quads.length);
console.log('graph IRI:', quads[0]?.graph.value);
Output
Press Execute to run this example against the real library.

Both options can be paired with registry-level defaults via JsonTology.create({ iriFor, defaultGraphIri }): see Static helpers - graph emission options.

jt.fromQuads

Declaration. Inverse of toQuads. Given an array of quads and a target schema reference ($id string or schema object with $id), returns an array of validated typed objects. Each returned value runs through instantiate, so defaults are applied, transforms execute, and validation errors throw InstantiationError.

Use this when quads arrive from an external source - a triple store, a reasoner output, a federated query, an inbound RDF payload - and you want them as typed JS objects. The return is an array because a single subject set can contain multiple instances of the target class.

Don't use this when you already have JS objects in hand (use instantiate directly). Don't use it on quads with no rdf:type or no recognizable predicates - lifting needs the property IRIs that match the target schema's graph.

Reversible skolemization

Pass { deskolemize: true } to treat IRIs matching the W3C well-known genid pattern (*/.well-known/genid/<hash>) as blank nodes during reconstruction. This pairs with Skolemize.wellKnownGenid on toQuads:

/**
 * Reversible skolemization via Skolemize.wellKnownGenid + fromQuads({ deskolemize }).
 *
 * The well-known genid pattern (W3C RDF 1.1 §3.5) produces deterministic IRIs
 * that fromQuads recognises and rewrites back to blank nodes during lift —
 * round-tripping blank-node identity across a wire-form serialization.
 */

import { Skolemize } from '../../../src/index.js';
import {
  aboxFixtures, bookstoreEntities, CustomerSchema
} from '../bookstore/index.js';

const quads = bookstoreEntities.toQuads(CustomerSchema, aboxFixtures.customer, { 'iriFor': Skolemize.wellKnownGenid('https://shop.example.com') });

// Round-trip back to blank-node semantics — use the string key form for full type inference
const restoredList = bookstoreEntities.fromQuads(CustomerSchema.$id, quads, { 'deskolemize': true });
const restored = restoredList[0];

if (restored === undefined) {
  throw new Error('expected restored customer');
}

console.assert(restored.customerId === aboxFixtures.customer.customerId, 'customer id round-tripped');
console.log('round-tripped customerId:', restored.customerId);
Output
Press Execute to run this example against the real library.

The registry-level defaultDeskolemize: true flips this on for every fromQuads call without per-call overrides.

Examples

Example 1: Round-trip an order

/**
 * Round-trip a customer through quads.
 *
 * Project the validated customer to RDF, then lift the quads back through
 * fromQuads. Each lifted object passes through instantiate, so defaults
 * and transforms apply on the return path.
 */

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

const original = bookstoreEntities.instantiate(CustomerSchema, aboxFixtures.customer);

const quads = bookstoreEntities.toQuads(CustomerSchema, original);
const restoredList = bookstoreEntities.fromQuads(CustomerSchema.$id, quads);
const restored = restoredList[0];

if (restored === undefined) {
  throw new Error('expected restored customer');
}

console.assert(restored.customerId === original.customerId, 'customer id round-tripped');
console.assert(restored.name === original.name, 'customer name round-tripped');
console.log('round-tripped customer:', restored.name, '/', restored.customerId);
Output
Press Execute to run this example against the real library.

Example 2: Lift quads from a triple store

/**
 * Lift quads from an external source into typed Customer objects.
 *
 * In production, quads arrive from a SPARQL CONSTRUCT, a DESCRIBE query,
 * a reasoner output, or an inbound RDF payload. Here we simulate the
 * external source by projecting a customer through toQuads first, then
 * lifting that quad set back through fromQuads. fromQuads returns a
 * Customer[] — validated, typed, defaults applied.
 */

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

// Stand-in for `await fetchQuadsFromTripleStore('... WHERE { ?c a :Customer }')`.
const externalQuads = bookstoreEntities.toQuads(CustomerSchema, aboxFixtures.customer);

// fromQuads returns Customer[] — validated, typed, defaults applied.
// Use the string key form for full type inference on the returned array.
const customers = bookstoreEntities.fromQuads(CustomerSchema.$id, externalQuads);

console.assert(customers.length > 0, 'lifted at least one Customer individual');
for (const customer of customers) {
  console.assert(typeof customer.name === 'string', 'customer.name lifted as string');
  console.assert(typeof customer.email === 'string', 'customer.email lifted as string');
}
console.log('lifted customers:', customers.length);
console.log('first customer name:', customers[0]?.name);
Output
Press Execute to run this example against the real library.

fromQuads subject-type dispatch

When a combined quad set contains individuals of more than one class, fromQuads uses each subject's rdf:type triple to dispatch to the correct schema. Calling fromQuads with EBookSchema.$id lifts only EBook subjects; calling it with PrintBookSchema.$id lifts only PrintBook subjects.

/**
 * `fromQuads` subject-type dispatch — lifting EBook and PrintBook individuals.
 *
 * When a quad set contains individuals of different types, `fromQuads` uses
 * the `rdf:type` triple on each subject to dispatch to the correct schema.
 * Calling `fromQuads` with `EBookSchema.$id` lifts only EBook subjects;
 * calling it with `PrintBookSchema.$id` lifts only PrintBook subjects.
 *
 * This example builds a combined quad set from both an EBook and a PrintBook
 * fixture, then lifts each back through its own schema independently.
 */

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

// ── Project each individual to quads ──────────────────────────────────────
const ebook = bookstoreEntities.instantiate(EBookSchema, aboxFixtures.ebook);
const printBook = bookstoreEntities.instantiate(PrintBookSchema, aboxFixtures.printBook);

const ebookQuads = bookstoreEntities.toQuads(EBookSchema, ebook);
const printBookQuads = bookstoreEntities.toQuads(PrintBookSchema, printBook);

// Combine into one flat quad array — as if both arrived from a SPARQL CONSTRUCT.
const combinedQuads = [
  ...ebookQuads,
  ...printBookQuads
];

console.assert(combinedQuads.length > 0, 'combined quad set is non-empty');

// ── Lift by schema — subject-type dispatch ────────────────────────────────
// fromQuads filters by rdf:type and instantiates matching subjects only.
const liftedEbooks = bookstoreEntities.fromQuads(EBookSchema.$id, combinedQuads);
const liftedPrintBooks = bookstoreEntities.fromQuads(PrintBookSchema.$id, combinedQuads);

console.assert(liftedEbooks.length === 1, 'exactly one EBook lifted from combined quads');
console.assert(liftedPrintBooks.length === 1, 'exactly one PrintBook lifted from combined quads');

// ── Verify field values round-tripped correctly ───────────────────────────
if (liftedEbooks.length === 0 || liftedPrintBooks.length === 0) {
  throw new Error('expected lifted instances not found');
}

const liftedEbook = liftedEbooks[0];
const liftedPrint = liftedPrintBooks[0];

if (liftedEbook === undefined || liftedPrint === undefined) {
  throw new Error('expected lifted instances not found');
}

console.assert(
  liftedEbook.isbn === aboxFixtures.ebook.isbn,
  'EBook isbn round-tripped'
);
console.assert(
  liftedEbook.fileFormat === aboxFixtures.ebook.fileFormat,
  'EBook fileFormat round-tripped'
);

// downloadUrl was emitted as a NamedNode (x-jt-iriRef: true); fromQuads
// lifts the IRI back as a plain string value — the JS type is string in both directions.
console.assert(
  liftedEbook.downloadUrl === aboxFixtures.ebook.downloadUrl,
  'EBook downloadUrl round-tripped from NamedNode'
);

console.assert(
  liftedPrint.isbn === aboxFixtures.printBook.isbn,
  'PrintBook isbn round-tripped'
);
console.assert(
  liftedPrint.binding === aboxFixtures.printBook.binding,
  'PrintBook binding round-tripped'
);

console.log('Combined quad set size:', combinedQuads.length);
console.log('\nLifted EBook:');
console.log('  isbn:', liftedEbook.isbn);
console.log('  fileFormat:', liftedEbook.fileFormat);
console.log('  downloadUrl:', liftedEbook.downloadUrl);

console.log('\nLifted PrintBook:');
console.log('  isbn:', liftedPrint.isbn);
console.log('  binding:', liftedPrint.binding);
Output
Press Execute to run this example against the real library.

Static counterparts

Both methods have static counterparts on JsonTology for one-shot use without a long-lived registry. The static variants build an ephemeral registry containing only the supplied schema, run the operation, and discard the registry.

/**
 * Static counterparts: JsonTology.toQuads / JsonTology.fromQuads.
 *
 * Static variants build an ephemeral registry containing only the supplied
 * schema, run the operation, and discard the registry. Use them when the
 * schema is self-contained ($ref free) and the projection runs once.
 *
 * The bookstore's IsbnSchema is a self-contained string primitive — its
 * graph has no $ref edges, so it round-trips through the static API
 * without registering its sibling schemas.
 *
 * Note: object-shaped bookstore entities (Customer, Order, Book) carry
 * $refs to other registered primitives and would throw GraphError on the
 * static API. Use a long-lived JsonTology instance for those.
 */

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

const isbn = '9783522128001';
const quads = JsonTology.toQuads(IsbnSchema, isbn);

console.assert(quads.length >= 0, 'static toQuads returned a quad array');

const restored = JsonTology.fromQuads(IsbnSchema, quads);

console.assert(Array.isArray(restored), 'static fromQuads returned an array');
console.log('static toQuads quad count:', quads.length);
console.log('static fromQuads result:', restored[0]);
Output
Press Execute to run this example against the real library.

Use the static form when:

  • The schema is self-contained and does not $ref other registered schemas.
  • You do not plan to project the same schema repeatedly (each call rebuilds the graph).

For repeated projections, hold onto a JsonTology instance.

Comparison

ToolABox round-trip
json-tology toQuads / fromQuadsSymmetric round-trip through the canonical graph
ZodNo equivalent
TypeBox + ValueNo equivalent
AJVNo equivalent
PydanticNo equivalent

This capability is unique to json-tology because the runtime representation is already a graph - validation, materialization, and ABox projection all consume the same node and relation structure.

rdf/js ecosystem interop

json-tology produces rdf/js-spec quads directly. No conversion required.

QuadInterface is a re-export of @rdfjs/types#Quad. Quads carry the rdf/js-spec termType: 'Quad', value: '', and equals(other).

  • subject is a NamedNode or BlankNode
  • predicate is a NamedNode
  • object is a NamedNode, BlankNode, or Literal
  • graph is a NamedNode, BlankNode, or DefaultGraph
  • Literal.value is string per the rdf/js spec; the JS type tag is carried in Literal.datatype.value (xsd:integer, xsd:boolean, xsd:dateTime, etc.)

@rdfjs/types is a runtime dependency of json-tology (types-only, zero runtime cost), so you can import type { Quad } from '@rdfjs/types' without a separate install.

Piping to n3.Writer

ts
import { Writer } from 'n3';

const jt = JsonTology.create({ baseIri: 'https://bookstore.example', schemas: [CustomerSchema] as const });
const quads = jt.toQuads(CustomerSchema, { id: 'cust-1', name: 'Alice', email: 'alice@example.com' });

// No cast required — QuadInterface is @rdfjs/types#Quad.
const writer = new Writer();
writer.addQuads(quads);
writer.end((_err, result) => console.log(result));

Terms factory

The in-house Terms factory (src/modules/rdf/Terms.ts) produces rdf/js-spec term objects (NamedNode, BlankNode, Literal, DefaultGraph) and quads (@rdfjs/types#Quad) without requiring @rdfjs/data-model at runtime. To use a different DataFactory (e.g. n3.DataFactory, @rdfjs/data-model), construct quads with that factory and pass them into jt.fromQuads(). They are accepted as-is because the project's accepted shape is the canonical rdf/js spec.

Recovering typed JS values from literals

Literal.value is string per the rdf/js spec. The original JS type (number, boolean, Date) is carried in Literal.datatype.value (xsd:integer, xsd:boolean, xsd:dateTime, …). To recover the typed JS value, call Terms.decodeLiteral(literal):

/**
 * Terms.decodeLiteral — recover typed JS values from rdf/js Literal terms.
 *
 * Per the rdf/js spec, `Literal.value` is `string`. The original JS type
 * (number, boolean, Date) lives in `Literal.datatype.value` (`xsd:integer`,
 * `xsd:boolean`, `xsd:dateTime`, etc.). `Terms.decodeLiteral(literal)` reads
 * the datatype tag and parses the string back into a typed JS value. `fromQuads`,
 * the OWL importer, and `JsonLdFormatter` call it automatically — consumers
 * only need it when reading quads directly off `toQuads()` output.
 */

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

// xsd:integer datatype tag
const ageLiteral = Terms.literal(30);
// xsd:boolean datatype tag
const activeLiteral = Terms.literal(true);
// xsd:string datatype tag
const nameLiteral = Terms.literal('Bastian');

console.assert(ageLiteral.value === '30', 'spec value is string');
console.assert(Terms.decodeLiteral(ageLiteral) === 30, 'decoded as number');
console.assert(Terms.decodeLiteral(activeLiteral) === true, 'decoded as boolean');
console.assert(Terms.decodeLiteral(nameLiteral) === 'Bastian', 'string passes through');

console.log('Literal.value:', typeof ageLiteral.value, ageLiteral.value);
console.log('Terms.decodeLiteral:', typeof Terms.decodeLiteral(ageLiteral), Terms.decodeLiteral(ageLiteral));
Output
Press Execute to run this example against the real library.

fromQuads, the internal Lift pipeline, and the OWL import dispatchers call Terms.decodeLiteral automatically, so consumers using those entry points never have to hand-decode.

RDF lists

RDF lists (used by owl:unionOf, owl:oneOf, sh:or, sh:in, etc.) are emitted as the standard rdf:first / rdf:rest / rdf:nil triple chain. There is no project-internal "list term". The list head (a BlankNode) appears in the parent triple's object position and the chain materialises as additional quads.

Use Lists.build(items) to assemble a list as the object of a quad, and Lists.collect(head, allQuads) to walk the chain back into an item array:

/**
 * Lists.build / Lists.collect — RDF list emission and walking.
 *
 * RDF lists are encoded as the standard `rdf:first` / `rdf:rest` / `rdf:nil`
 * triple chain. There is no project-internal "list term" — the list head
 * (a BlankNode) appears in the parent triple's object position and the chain
 * materialises as additional quads.
 *
 * `Lists.build(items)` returns `{ head, triples }` so the caller can attach
 * `head` to the parent triple and concatenate `triples` into the output quad
 * array. `Lists.collect(head, allQuads)` walks the chain back into an item
 * array.
 */

import {
  Lists, Terms
} from '../../../src/index.js';

const SHAPE = 'https://example.com/Shape';
const SH_OR = 'http://www.w3.org/ns/shacl#or';

// Emit: ex:Shape sh:or ( ex:Circle ex:Square ) .
const {
  head, triples
} = Lists.build([
  Terms.iri('https://example.com/Circle'),
  Terms.iri('https://example.com/Square')
]);

const quads = [
  Terms.quad(Terms.iri(SHAPE), Terms.iri(SH_OR), head),
  ...triples
];

console.assert(quads.length === 5, 'parent quad + 2 rdf:first + 2 rdf:rest');
console.assert(head.termType === 'BlankNode', 'head is a blank node');

// Walk the list back into an item array.
const items = Lists.collect(head, quads);

console.assert(items.length === 2, 'walked two items');
const item0 = items[0];
const item1 = items[1];

if (item0 === undefined || item1 === undefined) {
  throw new Error('expected two walked items');
}

console.assert(item0.termType === 'NamedNode' && item0.value === 'https://example.com/Circle');
console.assert(item1.termType === 'NamedNode' && item1.value === 'https://example.com/Square');

console.log('list head:', head.value);
console.log('walked items:', items.map((item) => {
  return item.value;
}));
Output
Press Execute to run this example against the real library.
  • toTbox - class and property declarations (TBox companion to ABox toQuads)
  • toShacl - structural shape constraints
  • ontology - combined TBox + SHACL
  • instantiate - the validator that runs on each lifted object

See also

Released under the MIT License.