JsonTology.prototype.sameAs Runtime
Declaration. Records an owl:sameAs assertion between two individuals (ABox-level identity). Both IRIs denote the same real-world entity. Emitted at toQuads() time as a pair of symmetric quads.
/**
* Signature of JsonTology.prototype.sameAs.
*
* Records an owl:sameAs assertion between two individuals. Both IRIs
* denote the same real-world entity. Emitted at toQuads() time as a
* pair of symmetric quads.
*/
import { bookstoreEntities } from '../bookstore/index.js';
// Type assertion: sameAs takes two IRI strings and returns void.
const sameAs: (instanceIriA: string, instanceIriB: string) => void = (instanceIriA, instanceIriB) => {
bookstoreEntities.sameAs(instanceIriA, instanceIriB);
};
sameAs('urn:bookstore:demo:a', 'urn:bookstore:demo:b');
console.assert(typeof sameAs === 'function', 'sameAs is callable with the documented signature');
console.log('sameAs type:', typeof sameAs);
Use this when you want to declare that two distinct IRIs refer to the same real-world entity. The canonical use is linking a current stable IRI to a legacy IRI after a system migration, or linking identifiers across two authoritative sources (e.g. an internal customer ID alongside a third-party marketplace ID). This is the ABox counterpart to Compose.equivalent (which is class-level via owl:equivalentClass).
Don't use this when the two schemas have different structure or you need class-level identity (use Compose.equivalent). Don't use it to express that two records should be merged - sameAs asserts they already refer to the same individual and downstream reasoners will treat their property values as belonging to one entity. Don't try to declare an instance is sameAs a class; OWL forbids cross-level identity.
Examples
Example 1: Link a legacy CRM identifier to a stable customer IRI
The bookstore migrated from a legacy CRM in 2024. Customer Bastian Balthazar Bux carries the current bookstore IRI alongside the legacy CRM ID (cust-00042) the bookstore inherited from the old system. Declaring sameAs lets a reasoner merge facts about both. The new email from the bookstore and the old purchase history from the CRM resolve to one logical individual.
/**
* Link a legacy CRM identifier to a stable customer IRI.
*
* The bookstore migrated from a legacy CRM in 2024. Bastian carries the
* current bookstore IRI alongside the legacy CRM ID (`cust-00042`).
* Declaring sameAs lets a reasoner merge facts from both authoritative
* sources into one logical individual.
*/
import {
aboxFixtures, bookstoreEntities, CustomerSchema
} from '../bookstore/index.js';
bookstoreEntities.sameAs(
'urn:bookstore:customer:bastian-bux',
'urn:legacy-crm:cust-00042'
);
const quads = bookstoreEntities.toQuads(CustomerSchema, aboxFixtures.customer);
// quads include both directions:
// <urn:bookstore:customer:bastian-bux> owl:sameAs <urn:legacy-crm:cust-00042>
// <urn:legacy-crm:cust-00042> owl:sameAs <urn:bookstore:customer:bastian-bux>
const sameAsQuads = quads.filter((quad) => {
return quad.predicate.value === 'http://www.w3.org/2002/07/owl#sameAs';
});
console.assert(sameAsQuads.length >= 2, 'symmetric owl:sameAs emitted');
console.log('owl:sameAs quads emitted:', sameAsQuads.length);
Example 2: Cross-catalog book identity
Bastian ordered a rare first-edition Michael Ende's Die unendliche Geschichte (Klett Books, 1979). The bookstore catalogs it under one IRI; WorldCat's union catalog references the same physical edition under an OCLC record IRI. Declaring sameAs lets a bibliographic reasoner unify metadata (publisher, page count, ISBN-13) regardless of which authority the fact came from.
/**
* Cross-catalog book identity via owl:sameAs.
*
* The bookstore catalogs the rare 1979 Thienemann first edition of
* Michael Ende's *Die unendliche Geschichte* under one IRI; WorldCat's
* union catalog references the same physical edition under an OCLC
* record IRI. Declaring sameAs lets a bibliographic reasoner unify
* metadata across authorities.
*
* The bookstore registry already declares this pair in `bookstore/index.ts`,
* so re-asserting it here is idempotent. The assertion is preserved in
* the registry's sameAsStore and emitted alongside any future toQuads
* call that surfaces the rare book individual.
*/
import { bookstoreEntities } from '../bookstore/index.js';
bookstoreEntities.sameAs(
'urn:bookstore:rarebook:unendlichegeschichte-1979-klett',
'http://www.worldcat.org/oclc/644849'
);
// Both IRIs now resolve to the same rare-book individual across the
// internal catalog and any partner reasoner that consults WorldCat.
// The pair is recorded in the registry's sameAsStore.
console.assert(true, 'cross-catalog sameAs pair recorded');
console.log('cross-catalog sameAs pair recorded: urn:bookstore:rarebook:unendlichegeschichte-1979-klett ↔ http://www.worldcat.org/oclc/644849');
Example 3: Idempotence: duplicate and reverse pairs are no-ops
Recording the same pair twice, or in reverse order, is a no-op. Self-pairs are silently dropped.
/**
* sameAs is idempotent — duplicate and reverse pairs are no-ops.
*
* Recording the same pair twice, or in reverse order, is a no-op.
* Self-pairs (a sameAs a) are silently dropped.
*/
import {
aboxFixtures, createBookstoreDocRegistry, CustomerSchema
} from '../bookstore/index.js';
const registry = createBookstoreDocRegistry();
registry.sameAs(
'urn:bookstore:customer:bastian-bux',
'urn:legacy-crm:cust-00042'
);
// No-op — pair already recorded.
registry.sameAs(
'urn:legacy-crm:cust-00042',
'urn:bookstore:customer:bastian-bux'
);
// No-op — self-pair.
registry.sameAs(
'urn:bookstore:customer:bastian-bux',
'urn:bookstore:customer:bastian-bux'
);
const quads = registry.toQuads(CustomerSchema, aboxFixtures.customer);
// Only the single recorded pair contributes quads (forward + reverse = 2).
const sameAsQuads = quads.filter((quad) => {
return quad.predicate.value === 'http://www.w3.org/2002/07/owl#sameAs'
&& (quad.subject.value === 'urn:bookstore:customer:bastian-bux' || quad.subject.value === 'urn:legacy-crm:cust-00042');
});
console.assert(sameAsQuads.length === 2, 'idempotent — only forward + reverse emitted');
console.log('sameAs quad count (idempotent):', sameAsQuads.length, '(duplicate and reverse pair dropped)');
Example 4: Symmetric emission
owl:sameAs is symmetric by definition, but reasoners differ in whether they materialize the symmetric edge. sameAs emits both directions so consumers see the relation regardless of reasoner behaviour.
/**
* owl:sameAs is symmetric — toQuads emits both directions explicitly.
*
* Reasoners differ in whether they materialize the symmetric edge.
* json-tology emits both directions at toQuads() time so consumers see
* the relation regardless of reasoner behaviour.
*/
import {
aboxFixtures, createBookstoreDocRegistry, CustomerSchema
} from '../bookstore/index.js';
const registry = createBookstoreDocRegistry();
registry.sameAs(
'urn:bookstore:customer:bastian-bux',
'urn:legacy-crm:cust-00042'
);
const quads = registry.toQuads(CustomerSchema, aboxFixtures.customer);
const sameAsQuads = quads.filter((quad) => {
return quad.predicate.value === 'http://www.w3.org/2002/07/owl#sameAs';
});
// Both directions always emitted.
console.assert(sameAsQuads.length === 2, 'both directions emitted');
const subjects = new Set(sameAsQuads.map((quad) => {
return quad.subject.value;
}));
console.assert(subjects.has('urn:bookstore:customer:bastian-bux'), 'forward subject present');
console.assert(subjects.has('urn:legacy-crm:cust-00042'), 'reverse subject present');
console.log('symmetric owl:sameAs subjects:', [...subjects]);
Bad examples: what NOT to do
Anti-pattern 1: Using sameAs for class-level identity
/**
* Anti-pattern: using sameAs for class-level identity.
*
* owl:sameAs is for individuals; OWL forbids it between class URIs.
* Use Compose.equivalent for class-level identity instead — it produces
* an owl:equivalentClass axiom.
*/
import { Compose } from '../../../src/index.js';
import {
BookSchema, bookstoreEntities
} from '../bookstore/index.js';
// WRONG — sameAs is for individuals; passing class IRIs produces
// invalid RDF (OWL DL rejects owl:sameAs between class URIs).
bookstoreEntities.sameAs(
'https://bookstore.example/Book',
'https://bookstore.example/CatalogItem'
);
// RIGHT — use Compose.equivalent for class-level identity.
const CatalogItemSchema = Compose.equivalent(BookSchema, { '$id': 'https://bookstore.example/CatalogItem' });
// Widen to string to avoid no-unnecessary-condition on literal-typed $id/$ref.
const catalogId: string = CatalogItemSchema.$id;
const catalogRef: string | undefined = CatalogItemSchema.$ref;
console.assert(catalogId === 'https://bookstore.example/CatalogItem', 'class alias defined');
console.assert(catalogRef === BookSchema.$id, 'thin $ref alias of Book');
console.log('CatalogItem $id:', catalogId);
console.log('CatalogItem $ref (resolves to Book):', catalogRef);
Anti-pattern 2: Declaring sameAs between two editions of the same title
/**
* Anti-pattern: declaring sameAs between two editions of the same title.
*
* sameAs asserts identity of individuals, not "they share a title".
* The 1979 Thienemann first edition and the 1984 Penguin English
* translation of Die unendliche Geschichte are two distinct physical
* books with different ISBNs, publishers, page counts, prices, and
* condition notes. They share an author and a title — that is what
* Compose.equivalent / shared $ref to the title primitive expresses
* at the class level, NOT what sameAs expresses at the instance level.
*/
import { bookstoreEntities } from '../bookstore/index.js';
// WRONG — collapses two physically distinct books into one logical
// individual. A reasoner consuming both edges will treat one book as
// having two ISBNs, two publishers, two prices, and two condition
// reports, silently corrupting the catalog. Do NOT do this:
// bookstoreEntities.sameAs(
// 'urn:bookstore:rarebook:neverending-1979-thienemann',
// 'urn:bookstore:rarebook:neverending-1984-penguin'
// );
// RIGHT — use sameAs only across two IRIs that authoritatively name the
// same physical or logical individual (one record in two systems, one
// customer across a migration, one book in two union catalogs).
bookstoreEntities.sameAs(
'urn:bookstore:rarebook:neverending-1979-thienemann',
'http://www.worldcat.org/oclc/5705614'
);
// The recorded pair is now in the registry's sameAsStore — toQuads
// (called from any later page that emits ABox quads) will emit both
// directions of the owl:sameAs link symmetrically.
console.assert(true, 'sameAs assertion recorded against authoritative pair');
console.log('sameAs recorded: urn:bookstore:rarebook:neverending-1979-thienemann ↔ http://www.worldcat.org/oclc/5705614');
Anti-pattern 3: Calling sameAs after toQuads instead of before
/**
* Anti-pattern: calling sameAs after toQuads instead of before.
*
* sameAs assertions only contribute quads to the toQuads() call that
* follows them. A sameAs recorded after toQuads is too late — the
* earlier quad set has already been emitted without the identity link.
*/
import {
aboxFixtures, createBookstoreDocRegistry, CustomerSchema
} from '../bookstore/index.js';
const registry = createBookstoreDocRegistry();
// WRONG — record the assertion AFTER projecting.
const tooEarly = registry.toQuads(CustomerSchema, aboxFixtures.customer);
registry.sameAs(
'urn:bookstore:customer:bastian-bux',
'urn:legacy-crm:cust-00042'
);
const tooEarlySameAs = tooEarly.filter((quad) => {
return quad.predicate.value === 'http://www.w3.org/2002/07/owl#sameAs';
});
console.assert(tooEarlySameAs.length === 0, 'first projection missed the sameAs link');
console.log('sameAs quads in early projection (should be 0):', tooEarlySameAs.length);
// RIGHT — record sameAs assertions BEFORE calling toQuads.
const onTime = registry.toQuads(CustomerSchema, aboxFixtures.customer);
const onTimeSameAs = onTime.filter((quad) => {
return quad.predicate.value === 'http://www.w3.org/2002/07/owl#sameAs';
});
console.assert(onTimeSameAs.length >= 2, 'second projection includes the sameAs link');
console.log('sameAs quads in on-time projection:', onTimeSameAs.length);
Comparison
bookstoreEntities.sameAs('urn:bookstore:customer:bastian-bux', 'urn:legacy-crm:cust-00042');
// Emits both directions at toQuads() time as owl:sameAs quads.// Zod has no RDF or identity layer. There is no equivalent concept.
// You would need a separate RDF library (n3, rdflib) to assert owl:sameAs.// Valibot has no RDF or identity layer. No equivalent concept.// AJV is a validator only — no graph model, no RDF output.
// Emit owl:sameAs manually in your RDF serialization layer.import { graph, sym, Statement } from 'rdflib';
const store = graph();
const OWL = 'http://www.w3.org/2002/07/owl#';
const bastian = sym('urn:bookstore:customer:bastian-bux');
const bastianCrm = sym('urn:legacy-crm:cust-00042');
store.add(new Statement(bastian, sym(`${OWL}sameAs`), bastianCrm, sym('urn:g')));
store.add(new Statement(bastianCrm, sym(`${OWL}sameAs`), bastian, sym('urn:g')));
// Limitation: both directions must be added manually; no schema validation
// or typed instance pipeline; standalone RDF store without JSON Schema authoring.from rdflib import Graph, URIRef, OWL
g = Graph()
bastian = URIRef('urn:bookstore:customer:bastian-bux')
bastian_crm = URIRef('urn:legacy-crm:cust-00042')
g.add((bastian, OWL.sameAs, bastian_crm))
g.add((bastian_crm, OWL.sameAs, bastian))
# Limitation: manual symmetric emission; no schema-validated pipeline.Future / not implemented
owl:differentFrom: the negation of sameAs. Tracked separately.
Related
Compose.equivalent-owl:equivalentClassfor class identity- OWL class restrictions - TBox-level constraints
- Graph concepts (TBox / ABox)
toQuads/fromQuads
See also
- Bookstore domain - schema definitions used in examples