OWL 2 property characteristics Compile-time + Runtime
OWL 2 defines seven property characteristics that describe logical properties of object properties in a TBox. json-tology supports all seven. Setting a characteristic keyword to true on a property schema emits the corresponding rdf:type owl:*Property quad through the canonical graph.
Supported keywords
| Keyword | OWL 2 characteristic | Emitted quad |
|---|---|---|
symmetric | owl:SymmetricProperty | <prop> rdf:type owl:SymmetricProperty |
transitive | owl:TransitiveProperty | <prop> rdf:type owl:TransitiveProperty |
asymmetric | owl:AsymmetricProperty | <prop> rdf:type owl:AsymmetricProperty |
functional | owl:FunctionalProperty | <prop> rdf:type owl:FunctionalProperty |
inverseFunctional | owl:InverseFunctionalProperty | <prop> rdf:type owl:InverseFunctionalProperty |
irreflexive | owl:IrreflexiveProperty | <prop> rdf:type owl:IrreflexiveProperty |
reflexive | owl:ReflexiveProperty | <prop> rdf:type owl:ReflexiveProperty |
All seven keywords are registered in KNOWN_SCHEMA_KEYWORDS (src/constants/SCHEMA_KEYWORDS.ts); the emit logic that converts them to rdf:type owl:*Property quads lives in src/modules/graph/SchemaGraphRelations.ts. They are also exposed as fields on SchemaGraphSemanticsInterface so consumers can inspect them on the canonical graph without re-reading the source schema.
Usage
Declare a characteristic on the property's schema body. The keyword is set at the property-schema level, not the class level.
/**
* OWL 2 property characteristics — emit owl:*Property axioms via toTbox().
*
* The bookstore demonstrates the seven OWL 2 property characteristics
* through real entities:
* SimilarBook.b symmetric + reflexive
* Sequel.predecessor asymmetric
* Order.placedAt transitive + irreflexive
* Customer.id inverseFunctional
*
* Calling toTbox().jsonLd() emits the rdf:type owl:*Property quads on the
* appropriate property IRIs.
*/
import { bookstoreEntities } from '../bookstore/index.js';
const tboxJsonLd = bookstoreEntities.toTbox().jsonLd();
// The TBox is a JSON-LD string containing class and property declarations.
// Property entries carry @type arrays like:
// ["owl:ObjectProperty", "owl:SymmetricProperty", "owl:ReflexiveProperty"]
console.assert(typeof tboxJsonLd === 'string', 'TBox JSON-LD emitted');
console.assert(tboxJsonLd.includes('SymmetricProperty'), 'symmetric axioms present');
console.assert(tboxJsonLd.includes('AsymmetricProperty'), 'asymmetric axioms present');
console.assert(tboxJsonLd.includes('TransitiveProperty'), 'transitive axioms present');
console.assert(tboxJsonLd.includes('InverseFunctionalProperty'), 'inverseFunctional axioms present');
console.log('TBox JSON-LD byte length:', tboxJsonLd.length);
console.log('SymmetricProperty present:', tboxJsonLd.includes('SymmetricProperty'));
console.log('AsymmetricProperty present:', tboxJsonLd.includes('AsymmetricProperty'));
console.log('TransitiveProperty present:', tboxJsonLd.includes('TransitiveProperty'));
console.log('InverseFunctionalProperty present:', tboxJsonLd.includes('InverseFunctionalProperty'));
The TBox output for KnowsSchema includes:
{
"@id": "https://example.com/knows",
"@type": ["owl:ObjectProperty", "owl:SymmetricProperty", "owl:TransitiveProperty"]
}Semantics
Property characteristics are TBox axioms - they describe logical structure and enable reasoning, they do not add runtime validation constraints.
symmetric: ifA knows BthenB knows A.transitive: ifA knows BandB knows CthenA knows C.asymmetric: ifA parentOf BthenB parentOf Ais false.functional: each subject has at most one value for this property.inverseFunctional: each value uniquely identifies its subject.irreflexive: no individual relates to itself via this property.reflexive: every individual relates to itself via this property.
Compile-time conflict detection
Three combinations are logically impossible under OWL 2 semantics. Setting them on the same property produces a PropertyCharacteristicConflictInterface branded type error at the property definition site, and SchemaRegistry.set() throws a SchemaError with code PROPERTY_CHARACTERISTIC_CONFLICT at runtime.
| Conflict | Reason |
|---|---|
symmetric + asymmetric | Mutually exclusive by definition - a relation cannot be both directed and undirected |
reflexive + irreflexive | Mutually exclusive by definition - an individual cannot both relate and not relate to itself |
asymmetric + reflexive | Asymmetric implies irreflexive in OWL 2; explicit reflexive directly contradicts that |
symmetric + asymmetric
/**
* Compile-time conflict: symmetric + asymmetric are mutually exclusive.
*
* ValidatePropertyCharacteristicsType brands the schema with
* PropertyCharacteristicConflictType when a property declares both.
* The @ts-expect-error directive proves the brand fires at the property
* definition site.
*/
import type { ValidatePropertyCharacteristicsType } from '../../../src/types/TypeErrors.js';
// @ts-expect-error — 'relates' sets symmetric:true and asymmetric:true
// (PropertyCharacteristicConflictType)
const _bad: ValidatePropertyCharacteristicsType<{
readonly '$id': 'urn:test:Bad';
readonly 'properties': {
readonly 'relates': { readonly 'asymmetric': true;
readonly 'symmetric': true };
};
readonly 'type': 'object';
}> = {
'$id': 'urn:test:Bad',
'properties': {
'relates': {
'asymmetric': true,
'symmetric': true
}
},
'type': 'object'
} as const;
void _bad;
// The @ts-expect-error above confirms the brand fires at the definition site.
// At runtime the object is structurally valid; the conflict is a compile-time guarantee.
console.log('symmetric+asymmetric conflict detected at compile time (PropertyCharacteristicConflictType)');
reflexive + irreflexive
/**
* Compile-time conflict: reflexive + irreflexive are mutually exclusive.
*
* An individual cannot both relate and not relate to itself.
* ValidatePropertyCharacteristicsType surfaces the conflict at compile time.
*/
import type { ValidatePropertyCharacteristicsType } from '../../../src/types/TypeErrors.js';
// @ts-expect-error — 'rel' sets reflexive:true and irreflexive:true
// (PropertyCharacteristicConflictType)
const _bad: ValidatePropertyCharacteristicsType<{
readonly '$id': 'urn:test:Bad';
readonly 'properties': {
readonly 'rel': { readonly 'irreflexive': true;
readonly 'reflexive': true };
};
readonly 'type': 'object';
}> = {
'$id': 'urn:test:Bad',
'properties': {
'rel': {
'irreflexive': true,
'reflexive': true
}
},
'type': 'object'
} as const;
void _bad;
// The @ts-expect-error above confirms the brand fires at the definition site.
// At runtime the object is structurally valid; the conflict is a compile-time guarantee.
console.log('reflexive+irreflexive conflict detected at compile time (PropertyCharacteristicConflictType)');
asymmetric + reflexive
/**
* Compile-time conflict: asymmetric + reflexive.
*
* Asymmetric implies irreflexive in OWL 2; an explicit reflexive
* directly contradicts that. The brand surfaces both characteristics
* in its `conflicts` tuple.
*/
import type { ValidatePropertyCharacteristicsType } from '../../../src/types/TypeErrors.js';
// @ts-expect-error — 'edge' sets asymmetric:true and reflexive:true
// (PropertyCharacteristicConflictType)
const _bad: ValidatePropertyCharacteristicsType<{
readonly '$id': 'urn:test:Bad';
readonly 'properties': {
readonly 'edge': { readonly 'asymmetric': true;
readonly 'reflexive': true };
};
readonly 'type': 'object';
}> = {
'$id': 'urn:test:Bad',
'properties': {
'edge': {
'asymmetric': true,
'reflexive': true
}
},
'type': 'object'
} as const;
void _bad;
// The @ts-expect-error above confirms the brand fires at the definition site.
// At runtime the object is structurally valid; the conflict is a compile-time guarantee.
console.log('asymmetric+reflexive conflict detected at compile time (PropertyCharacteristicConflictType)');
The brand interface shape is:
/**
* PropertyCharacteristicConflictType — brand shape.
*
* The brand interface carries the offending property name and the
* conflicting characteristics as a tuple. IDE hover on the
* failing assignment surfaces all three fields directly.
*/
import type { PropertyCharacteristicConflictType } from '../../../src/types/TypeErrors.js';
// Demonstrate the brand shape by typing a value that conforms to it.
const brand: PropertyCharacteristicConflictType<
'relates',
['symmetric', 'asymmetric']
> = {
'conflicts': [
'symmetric',
'asymmetric'
],
'kind': 'PropertyCharacteristicConflict',
'property': 'relates'
};
// Widen so the structural assertions are useful at runtime.
const widened: {
'conflicts': readonly string[];
'kind': string;
'property': string;
} = brand;
console.assert(widened.kind === 'PropertyCharacteristicConflict', 'kind discriminator');
console.assert(widened.property === 'relates', 'property name surfaced');
console.assert(widened.conflicts.length === 2, 'conflicts tuple');
console.log('brand.kind:', widened.kind);
console.log('brand.property:', widened.property);
console.log('brand.conflicts:', widened.conflicts.join(', '));
IDE hover on the failing assignment surfaces kind, property, and conflicts directly, making the offending property and characteristics visible without reading a stack trace.
Examples
/**
* Good combinations — bookstore patterns that demonstrate the legal
* pairings of OWL 2 property characteristics.
*
* SimilarBook.b symmetric + reflexive
* Sequel.predecessor asymmetric
* Order.placedAt transitive + irreflexive
*
* All three live in the registered bookstore graph. Re-exporting them
* here keeps the docs example anchored to the real schemas.
*/
import {
bookstoreEntities, OrderSchema, SequelSchema, SimilarBookSchema
} from '../bookstore/index.js';
// SimilarBook.b — symmetric + reflexive (book similarity is mutual and
// every book is trivially similar to itself).
console.assert(SimilarBookSchema.properties.b.symmetric, 'SimilarBook.b symmetric');
console.assert(SimilarBookSchema.properties.b.reflexive, 'SimilarBook.b reflexive');
// Sequel.predecessor — asymmetric (if Book A is the predecessor of Book B,
// then Book B is not the predecessor of Book A).
console.assert(SequelSchema.properties.predecessor.asymmetric, 'Sequel.predecessor asymmetric');
// Order.placedAt — transitive + irreflexive (timestamp ordering composes;
// an order cannot precede itself).
console.assert(OrderSchema.properties.placedAt.transitive, 'Order.placedAt transitive');
console.assert(OrderSchema.properties.placedAt.irreflexive, 'Order.placedAt irreflexive');
// All three propagate into the registered graph's TBox output.
const tboxJsonLd = bookstoreEntities.toTbox().jsonLd();
console.assert(tboxJsonLd.includes('SymmetricProperty'), 'SymmetricProperty emitted');
console.assert(tboxJsonLd.includes('AsymmetricProperty'), 'AsymmetricProperty emitted');
console.assert(tboxJsonLd.includes('TransitiveProperty'), 'TransitiveProperty emitted');
console.log('SimilarBook.b symmetric:', SimilarBookSchema.properties.b.symmetric);
console.log('Sequel.predecessor asymmetric:', SequelSchema.properties.predecessor.asymmetric);
console.log('Order.placedAt transitive:', OrderSchema.properties.placedAt.transitive);
console.log('Order.placedAt irreflexive:', OrderSchema.properties.placedAt.irreflexive);
console.log('All three characteristics emitted in TBox: true');
Bad examples: what NOT to do
/**
* Bad patterns — three OWL 2 property-characteristic combinations that
* are mutually exclusive. Each one is flagged at compile time by
* ValidatePropertyCharacteristicsType so the offending property is
* surfaced in IDE hover.
*
* These are NOT registered with the bookstore graph — they would be
* rejected at SchemaRegistry.set() with code PROPERTY_CHARACTERISTIC_CONFLICT.
*/
import type { ValidatePropertyCharacteristicsType } from '../../../src/types/TypeErrors.js';
// Bad 1 — symmetric + asymmetric are mutually exclusive.
// @ts-expect-error — PropertyCharacteristicConflictType<'relates', ['symmetric', 'asymmetric']>
const _bad1: ValidatePropertyCharacteristicsType<{
readonly '$id': 'urn:test:Bad1';
readonly 'properties': {
readonly 'relates': { readonly 'asymmetric': true;
readonly 'symmetric': true };
};
readonly 'type': 'object';
}> = {
'$id': 'urn:test:Bad1',
'properties': {
'relates': {
'asymmetric': true,
'symmetric': true
}
},
'type': 'object'
} as const;
// Bad 2 — reflexive + irreflexive are mutually exclusive.
// @ts-expect-error — PropertyCharacteristicConflictType<'rel', ['reflexive', 'irreflexive']>
const _bad2: ValidatePropertyCharacteristicsType<{
readonly '$id': 'urn:test:Bad2';
readonly 'properties': {
readonly 'rel': { readonly 'irreflexive': true;
readonly 'reflexive': true };
};
readonly 'type': 'object';
}> = {
'$id': 'urn:test:Bad2',
'properties': {
'rel': {
'irreflexive': true,
'reflexive': true
}
},
'type': 'object'
} as const;
// Bad 3 — asymmetric implies irreflexive; explicit reflexive contradicts it.
// @ts-expect-error — PropertyCharacteristicConflictType<'edge', ['asymmetric', 'reflexive']>
const _bad3: ValidatePropertyCharacteristicsType<{
readonly '$id': 'urn:test:Bad3';
readonly 'properties': {
readonly 'edge': { readonly 'asymmetric': true;
readonly 'reflexive': true };
};
readonly 'type': 'object';
}> = {
'$id': 'urn:test:Bad3',
'properties': {
'edge': {
'asymmetric': true,
'reflexive': true
}
},
'type': 'object'
} as const;
void _bad1;
void _bad2;
void _bad3;
// Each @ts-expect-error above confirms the conflict brand fires at the definition site.
// All three pairs are OWL 2 logical impossibilities detected at compile time.
console.log('bad patterns: symmetric+asymmetric, reflexive+irreflexive, asymmetric+reflexive all rejected at compile time');
Comparison
OWL DL reasoners detect these contradictions at query time - a reasoner run over a TBox that declares both owl:SymmetricProperty and owl:AsymmetricProperty on the same property will identify it as an inconsistency, but only after the ontology is loaded and reasoned over. json-tology surfaces the same contradiction at schema-authoring time, before any data touches the system.
Zod has no concept of OWL property characteristics and provides no equivalent enforcement layer - authors must discover logical impossibilities through runtime behavior or domain review rather than compiler output.
Constants
The IRI constants are exported from src/constants/IRI.ts:
/**
* OWL and RDFS IRI constants used by the canonical graph.
*
* The constants in src/constants/IRI.ts carry full absolute IRIs in the
* standard OWL and RDFS namespaces. Importing them directly lets consumers
* reference the same property identifiers used by the TBox projection.
*/
import {
OWL, RDFS
} from '../../../src/constants/IRI.js';
const OWL_NS = 'http://www.w3.org/2002/07/owl#';
const RDFS_NS = 'http://www.w3.org/2000/01/rdf-schema#';
const iris: Record<string, string> = {
'AsymmetricProperty': OWL.AsymmetricProperty,
'FunctionalProperty': OWL.FunctionalProperty,
'InverseFunctionalProperty': OWL.InverseFunctionalProperty,
'IrreflexiveProperty': OWL.IrreflexiveProperty,
'ReflexiveProperty': OWL.ReflexiveProperty,
'subPropertyOf': RDFS.subPropertyOf,
'SymmetricProperty': OWL.SymmetricProperty,
'TransitiveProperty': OWL.TransitiveProperty
};
for (const [
name,
iri
] of Object.entries(iris)) {
const expectedNs = name.startsWith('subProperty') ? RDFS_NS : OWL_NS;
console.assert(iri.startsWith(expectedNs), `${name} is in ${expectedNs} namespace: ${iri}`);
}
console.log('OWL.SymmetricProperty:', OWL.SymmetricProperty);
console.log('OWL.TransitiveProperty:', OWL.TransitiveProperty);
console.log('RDFS.subPropertyOf:', RDFS.subPropertyOf);
Related
- Ontology and graphs -
toTbox,toShacl,ontology - OWL class axioms -
subClassOf,disjointWith,complementOf - OWL property restrictions - cardinality,
someValuesFrom,allValuesFrom,hasValue
See also
- Bookstore domain - the running example domain
- Validation modes - enforcement layer reference