OWL logoDeep dive

What the OWL Coder Knows: The IR Map, the Extras, the Ledger

Ask a schema converter what it does with `owl:Restriction` and you learn most of what you need to know about it. Many treat OWL as a serialization problem — emit classes and properties, drop the axioms — so "required" and "single-valued" evaporate at the border. CoreModels treats it as a semantics problem: required and cardinality ride as `owl:Restriction` subclass axioms, OWL's own carrier, in **both** directions. That decision propagates through everything below.

What the OWL Coder Knows: The IR Map, the Extras, the Ledger

Ask a schema converter what it does with owl:Restriction and you learn most of what you need to know about it. Many treat OWL as a serialization problem — emit classes and properties, drop the axioms — so "required" and "single-valued" evaporate at the border. CoreModels treats it as a semantics problem: required and cardinality ride as owl:Restriction subclass axioms, OWL's own carrier, in both directions. That decision propagates through everything below.

This is the complete reference for the owl format key: what maps structurally, what rides in the annotation extras, the full lossiness inventory, and what a round trip preserves.

Everything on the CoreModels transform surface passes through one intermediate representation: Types (with single inheritance), Elements (typed, owned by types, carrying required and cardinality), Taxonomies (ordered term trees), Relations, Components, and an annotation bag per node holding provenance, cross-standard mapsTo assertions, and dotted-key extras. The OWL coder bridges that model and a Turtle document, both ways.

The structural map

IR constructOWL/Turtle form
Schemaan owl:Ontology subject (a full IRI minted under the entity namespace from the schema label) with rdfs:label
Typeowl:Class with rdfs:label, plus rdfs:comment when known
Type inheritancerdfs:subClassOf between two named classes
Element with a primitive valueowl:DatatypeProperty with an xsd: rdfs:range
Element referencing a Type or Taxonomyowl:ObjectProperty with that class as rdfs:range
Element owned by one Typerdfs:domain naming the owning class
Element reused across Typesrdfs:domain [ a owl:Class ; owl:unionOf ( … ) ]
Requiredrdfs:subClassOf [ a owl:Restriction ; owl:onProperty p ; owl:minCardinality "1"^^xsd:nonNegativeInteger ]
Single-valuedthe same shape with owl:maxCardinality "1"^^xsd:nonNegativeInteger
Collection with a max boundowl:maxCardinality "n"^^xsd:nonNegativeInteger
Taxonomya skos:ConceptScheme plus an owl:Class that is rdfs:subClassOf skos:Concept
Taxonomy terman individual typed with both the taxonomy class and skos:Concept, carrying skos:inScheme and skos:prefLabel
Term hierarchyskos:broader
Term cross-referenceskos:exactMatch
mapsTo on a Type or Elementthe entity's own IRI, plus owl:equivalentClass / owl:equivalentProperty for the remainder
Relationnarrowed to rdfs:seeAlso between the endpoints, with a ledger record
Componentnot exported; recorded as a structural drop

Decoding runs the same table backwards, with one classification step first: a class declaring rdfs:subClassOf skos:Concept becomes a Taxonomy, and everything else typed owl:Class becomes a Type. Taxonomies are built before types and properties, so an rdfs:range naming a concept class resolves to a taxonomy reference rather than a type reference.

Identity: every entity is already an IRI

This is the property OWL has that no other format on the surface does, and the coder leans on it in both directions.

Decoding. Each class and property contributes its own expanded IRI to the node's mapsTo set, recorded under the prefix it was written with. owl:equivalentClass, owl:equivalentProperty, and skos:exactMatch objects are added alongside under the standard owl. Decode ex:Product a owl:Class ; owl:equivalentClass schema:Product and the resulting Type carries both https://example.org/catalog#Product (standard ex) and https://schema.org/Product (standard owl) — identity you can match on later when aligning against another model.

Encoding. The entity IRI is chosen by three rules in order:

  1. If the node's id already has the shape of a prefixed name, it is used as-is. This is what makes decode-then-encode exact.
  2. Otherwise, the node's mapsTo URIs are reversed into a compact prefixed name via the longest matching namespace in the prefix map. A Type that arrived from JSON Schema carrying mapsTo → https://schema.org/Article therefore exports as schema:Article: the identity assertion becomes the name.
  3. Otherwise a fresh name is minted by PascalCasing the label under the coder's own entity namespace, written here as ex:https://coremodels.example.com/ns/.

A mapsTo URI that no namespace covers is emitted as a full IRI in angle brackets — owl:equivalentClass <http://purl.org/dc/dcmitype/Text>. The node's own IRI is skipped in the equivalence list, since naming the entity by it already asserts that identity.

The prefix map is assembled in layers: built-in defaults (rdf, rdfs, owl, xsd, skos, schema for schema.org, and the ex: minting namespace), overlaid with any prefixes the JSON-LD coder preserved — read, never written, so a model that arrived as JSON-LD exports under the prefixes it came with — overlaid with this coder's own preserved prefixes. Only prefixes actually used in the output appear in the header, sorted — plus the four core declarations (rdf, rdfs, owl, xsd), which are always emitted. The prefixed-name test in rule 1 is intentionally narrow — a colon after position zero, and no space, pipe, or dot — so that identifiers from other coders are never mistaken for prefixed names.

Cardinality: open world, honest bounds

The open-world default. A decoded property with no restrictions is optional and multi-valued. That is not a converter preference; it is what an unrestricted property means in RDF. If a downstream format shows a surprising optional array, the ontology never said otherwise.

Restrictions are the carrier, both directions. owl:minCardinality of 1 or more makes the element required. owl:maxCardinality 1 makes it single-valued; a larger maximum becomes the collection's upper bound. owl:cardinality n sets both bounds at once. Multiple restriction axioms on the same property merge strongest-wins: the largest minimum, the smallest maximum. A minimum above 1 is also carried through as the collection's lower bound.

One honest gap on the way out. OWL can say "at least one" through minCardinality 1, but a collection whose lower bound is higher — at least two keywords, say — has no carrier the encoder emits beyond that floor. The bound is relaxed and a ConstraintRelaxation record names the exact element:

[ConstraintRelaxation] Type[Doc].keyword: minItems 2 has no OWL carrier beyond the
min-cardinality-1 restriction; the bound is relaxed.

Datatypes

Decode: xsd: → IREncode: IR → xsd:
integer, int, long, short, nonNegativeInteger → IntegerInteger → xsd:integer
double, decimal, float → DoubleDouble → xsd:double
boolean → BooleanBoolean → xsd:boolean
dateTime, date, time → DateTimeDateTime → xsd:dateTime
everything else, including string and anyURI → StringString and RichText → xsd:string

The encode column looks lossier than it is: the decoder stashes the original range verbatim and the encoder prefers it. A property declared rdfs:range xsd:decimal decodes to the Double primitive and keeps xsd:decimal in the extras, so re-export says xsd:decimal. The generic column applies only to models that never came from OWL.

What rides the extras

Fidelity beyond the structural map travels in dotted, coder-prefixed extras — never smuggled into another format's output, always available for exact re-emit.

KeyScopeCarries
owl.prefix.<prefix>schemaeach prefix → namespace IRI from the decoded document
owl.commentany nodethe rdfs:comment text, re-emitted verbatim
owl.rangeelementthe original rdfs:range exactly as written, whenever the range is not a decoded class
owl.schemetaxonomya skos:ConceptScheme IRI that differs from the default naming convention
jsonld.context.<prefix>schemaread (never written) from the JSON-LD coder, for prefix continuity

The comment channel cooperates across coders on export: with no preserved OWL comment, the encoder falls back to a description another coder stashed, then to a SQL column comment, then to the node's instruction annotation — so human documentation reaches rdfs:comment whatever format the model came from.

The lossiness inventory

Export — what OWL cannot carry:

TriggerKindWhat the record says
ComponentStructuralDropOWL has no curated-view construct; the component projection is not exported
Element owned by no TypeStructuralDropexported without an rdfs:domain
RichText valueTypeApproximationrich text approximated as xsd:string
Collection lower bound above the required floorConstraintRelaxationthe bound is relaxed
Taxonomy marked external-referenceConstraintRelaxationthe directive has no OWL carrier; exported as a full concept scheme
Every RelationSemanticNarrowingnarrowed to rdfs:seeAlso; the group's semantics are not expressible in OWL

Import — what real-world Turtle brings:

TriggerKindBehavior
Well-formed triples outside the schema vocabularySemanticNarrowingcounted, reported as one summarized record — never an error
Language-tagged or repeated labels/commentsSemanticNarrowingfirst lexical form kept, alternatives recorded
Several rdfs:subClassOf parentsSemanticNarrowingnarrowed to the single IR parent
rdfs:subClassOf or rdfs:domain target that is not a decoded classSemanticNarrowingthe link or membership is dropped, naming the target
Several rdfs:range valuesSemanticNarrowingthe first is kept, and named
rdfs:range outside xsd: that is not a decoded classTypeApproximationapproximated as String, original kept in owl.range
Object property with no rdfs:rangeTypeApproximationapproximated as String
skos:broader target outside the taxonomySemanticNarrowingthe hierarchy link is dropped

Errors are reserved for documents that cannot be parsed at all — an empty file, an unterminated literal, an unclosed IRI, a truncated statement. They arrive on path $ — with a line number whenever there is a line to point at, for example Line 1: Unclosed IRI reference (missing '>'). — and never as an exception.

Round-trip fidelity

Encode a model to Turtle, decode it, and encode it again: the second document is byte-identical to the first. What survives a cycle exactly: labels; comments; the parent/child class link; required and single-valued facts, through the restriction axioms; a collection's upper bound; every mapsTo URI on types, elements, and terms; shared-element ownership across several types, through the union domain; the taxonomy's terms, hierarchy, and skos:exactMatch links; and the prefixes for every namespace the output uses.

What normalizes harmlessly: a bare owl:minCardinality 1 is re-emitted as "1"^^xsd:nonNegativeInteger; comma-separated rdfs:subClassOf lists become separate ; clauses; and a prefix declared but unused is not re-emitted.

Edge cases worth knowing

  • A plain-SKOS document decodes. A skos:ConceptScheme with no owl:Class carrier still becomes a taxonomy — the scheme itself plays that role, and its members are the concepts pointing at it with skos:inScheme.
  • rdfs:subClassOf owl:Thing is ignored, not reported. It says nothing.
  • rdfs:seeAlso closes the loop. Relations export as rdfs:seeAlso; on import, rdfs:seeAlso between two decoded schema nodes is rebuilt into a relation group, deduplicated. The wire form round-trips even though the export still records the narrowing — the group's original meaning genuinely is gone.
  • The default : prefix is a lossy choice. An entity written as :Person decodes with the id :Person, but a leading colon fails the prefixed-name test, so its IRI is not lifted into mapsTo and export re-mints it under ex:. Declare a named prefix and the identity survives.
  • Declare the namespaces you intend to map into. The encoder emits @prefix lines only for prefixes it can resolve from the model's preserved prefix map, so a mapping that retargets entities into a namespace your source never declared will use that prefix without declaring it. Adding the declaration to the source — even unused — makes the output self-contained.
  • Unrecognized blank nodes are counted, not guessed at. A blank node under rdfs:subClassOf that is not a restriction stays unconsumed and joins the summarized ignored-triple count.

The theme across all of it: OWL is expressive enough to carry most of the model natively, and wherever either side falls short the gap is written down — in the ledger, or in an extras key that lets export restore what decode had to approximate.

For hands-on usage of this coder over HTTP and MCP, start with the OWL quickstart in the CoreModels documentation.