Skip to content

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

KeywordOWL 2 characteristicEmitted quad
symmetricowl:SymmetricProperty<prop> rdf:type owl:SymmetricProperty
transitiveowl:TransitiveProperty<prop> rdf:type owl:TransitiveProperty
asymmetricowl:AsymmetricProperty<prop> rdf:type owl:AsymmetricProperty
functionalowl:FunctionalProperty<prop> rdf:type owl:FunctionalProperty
inverseFunctionalowl:InverseFunctionalProperty<prop> rdf:type owl:InverseFunctionalProperty
irreflexiveowl:IrreflexiveProperty<prop> rdf:type owl:IrreflexiveProperty
reflexiveowl: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'));
Output
Press Execute to run this example against the real library.

The TBox output for KnowsSchema includes:

json
{
  "@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: if A knows B then B knows A.
  • transitive: if A knows B and B knows C then A knows C.
  • asymmetric: if A parentOf B then B parentOf A is 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.

ConflictReason
symmetric + asymmetricMutually exclusive by definition - a relation cannot be both directed and undirected
reflexive + irreflexiveMutually exclusive by definition - an individual cannot both relate and not relate to itself
asymmetric + reflexiveAsymmetric 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)');
Output
Press Execute to run this example against the real library.

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)');
Output
Press Execute to run this example against the real library.

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)');
Output
Press Execute to run this example against the real library.

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(', '));
Output
Press Execute to run this example against the real library.

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');
Output
Press Execute to run this example against the real library.

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');
Output
Press Execute to run this example against the real library.

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);
Output
Press Execute to run this example against the real library.

See also

Released under the MIT License.