OWL Over HTTP: Export, Import, Map, Replay
Two questions decide whether a format is really supported: can the system read it, and can the system write it? For the `owl` format key in CoreModels the answer is yes to both, and this article is the proof — four routes, real request bodies, real responses, and the direction rules stated without hedging.
OWL Over HTTP: Export, Import, Map, Replay
Two questions decide whether a format is really supported: can the system read it, and can the system write it? For the owl format key in CoreModels the answer is yes to both, and this article is the proof — four routes, real request bodies, real responses, and the direction rules stated without hedging.
The honest headline first. The transform surface publishes two lists, and they are not identical:
- decode (import / source):
jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm - encode (export / target):
jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | synapse
Exactly two keys are one-way. odm decodes only — it is authored documentation, so there is nothing to encode back to. synapse encodes only — its output is plain draft-07 JSON Schema, which you re-import as jsonschema. owl is on neither list of exceptions. It appears in both, and everything below follows from that.
The surface
All routes live under {host}/graph/transform/ with no api/ prefix, require authentication, and are project-scoped and role-checked like every other CoreModels graph API. We use https://coremodels.example.com as the host placeholder and $TOKEN for the bearer token.
| Purpose | Route | Role |
|---|---|---|
| Export project schema as Turtle | POST graph/transform/schema/export/{projectId} | Viewer |
| Import Turtle into a project | POST graph/transform/schema/import/{projectId} | Admin |
| Map a schema through the engine (stateless) | POST graph/transform/schema/map/{projectId} | Viewer (ai mapping: Editor) |
| Replay a stored plan (stateless) | POST graph/transform/plan/execute/{projectId} | Viewer |
Every one of them answers with the same envelope:
{
"success": true, // it ran; false means it could not proceed — read "errors"
"lossiness": [], // what the target could not hold exactly; can be non-empty on success
"errors": [], // on failure: [{ "path": "…", "message": "…" }]
"schema": "…" // or "projectId", plus "plan" on the mapping routes
}
Two request options exist on this surface that never apply to Turtle: vendor (SQL output dialect) and the synapseOrg/synapseName/synapseVersion trio (the Synapse registered-schema $id). Formats ignore options they do not own, so passing them is harmless — but for owl there are no format-specific knobs at all. The optional spaces array on the import and export routes works as it does everywhere else: empty means the project's main space.
1. Export — the project as an ontology
curl -sS -X POST \
"https://coremodels.example.com/graph/transform/schema/export/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "format": "owl" }' | jq -r '.schema'
The schema field is a complete Turtle document. For a governed model holding one Product type with a required sku and an optional list_price, the encoder emits a sorted prefix header, an owl:Ontology subject carrying the schema's label, then one block per class and property:
@prefix ex: <https://coremodels.example.com/ns/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<https://coremodels.example.com/ns/ProductModel> a owl:Ontology ;
rdfs:label "Product Model" .
ex:Product a owl:Class ;
rdfs:label "Product" ;
rdfs:subClassOf [ a owl:Restriction ; owl:onProperty ex:Sku ; owl:minCardinality "1"^^xsd:nonNegativeInteger ] ;
rdfs:subClassOf [ a owl:Restriction ; owl:onProperty ex:Sku ; owl:maxCardinality "1"^^xsd:nonNegativeInteger ] ;
rdfs:subClassOf [ a owl:Restriction ; owl:onProperty ex:ListPrice ; owl:maxCardinality "1"^^xsd:nonNegativeInteger ] .
ex:Sku a owl:DatatypeProperty ;
rdfs:label "sku" ;
rdfs:domain ex:Product ;
rdfs:range xsd:string .
ex:ListPrice a owl:DatatypeProperty ;
rdfs:label "list_price" ;
rdfs:domain ex:Product ;
rdfs:range xsd:double .
Read the restriction blocks carefully, because they are the point. Required and single-valued are not stated in a CoreModels-specific annotation; they are expressed in OWL's own cardinality vocabulary, so any consumer that understands OWL understands the constraint without understanding us. Entity IRIs are minted under the ex: namespace from each node's label; the original label rides along as rdfs:label, so list_price survives even though the IRI is ex:ListPrice.
Export is a read: lossiness on this route reports what the project held that OWL could not carry. A curated component view comes back as StructuralDrop; a custom relation comes back as SemanticNarrowing because it is emitted as the weaker rdfs:seeAlso. Both arrive alongside "success": true, and both are true at once.
2. Import — an ontology as a project
Import is the only route here that writes, hence Admin. The body is format plus the Turtle text; rather than hand-escaping a document into a JSON string, let jq do it.
jq -n --rawfile ttl ontology.ttl '{ format: "owl", schema: $ttl }' \
| curl -sS -X POST \
"https://coremodels.example.com/graph/transform/schema/import/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data @-
For a well-formed ontology the response is the minimal envelope with the project id echoed back:
{ "success": true, "lossiness": [], "errors": [], "projectId": "…" }
If you prefer a one-liner to test the route, this body is small enough to paste directly:
{ "format": "owl", "schema": "@prefix : <https://example.org/onto#> .\n@prefix owl: <http://www.w3.org/2002/07/owl#> .\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n:Person a owl:Class .\n:name a owl:DatatypeProperty ; rdfs:domain :Person ; rdfs:range xsd:string .\n" }
Two decoder behaviors are worth knowing before you point this at an ontology you did not write.
Statements outside the schema vocabulary are lossiness, never errors. Real Turtle files carry Dublin Core dates, versioning metadata, annotation properties. The decoder counts every well-formed triple it did not consume and reports one summarized record — for example "2 triples outside the OWL schema vocabulary were ignored." Your import does not fail because someone added dcterms:created. Language-tagged labels behave the same way: rdfs:label "Person"@en , "Personne"@fr decodes to the first lexical form and records SemanticNarrowing for the rest.
Structurally broken Turtle fails with a line number. That is a different category, and it stops the run:
{
"success": false,
"lossiness": [],
"errors": [ { "path": "$", "message": "Line 1: Unclosed IRI reference (missing '>')." } ],
"ir": null
}
The parser handles the Turtle you actually meet in the wild: @prefix and SPARQL-style PREFIX directives, full IRIs and prefixed names, datatyped and language-tagged literals, ; and , continuations, blank nodes, collections, and # comments.
3. Map — anything in, Turtle out
schema/map is the mapping engine's stateless front door: decode the source, produce a plan against a target hint, validate the plan through the universal gate, execute, encode. Nothing touches the project, so every call is inherently a dry run. Because owl encodes, it works just as well as a target — here a warehouse table becomes an ontology.
DDL='CREATE TABLE Product (
sku VARCHAR(64) NOT NULL,
list_price NUMERIC,
launched_on TIMESTAMP
);'
jq -n --arg ddl "$DDL" '{
sourceFormat: "sql",
sourceSchema: $ddl,
targetFormat: "owl",
targetHintFormat: "sql",
targetHintSchema: $ddl,
mapping: { kind: "inferred" }
}' | curl -sS -X POST \
"https://coremodels.example.com/graph/transform/schema/map/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data @-
The response carries the Turtle as a string in schema (JSON-shaped targets return an object instead), an empty ledger for this input, and the executed plan:
{
"success": true,
"lossiness": [],
"errors": [],
"schema": "@prefix ex: <https://coremodels.example.com/ns/> .\n…",
"plan": {
"operations": [
{ "kind": "TypeMapping", "origin": "Inferred",
"sourceTypeId": "Product", "targetTypeId": "Product", "targetLabel": "Product" },
{ "kind": "ElementMapping", "origin": "Inferred",
"sourceElementIds": ["ProductSku"], "targetElementIds": ["ProductSku"] },
{ "kind": "ElementMapping", "origin": "Inferred",
"sourceElementIds": ["ProductListPrice"], "targetElementIds": ["ProductListPrice"] },
{ "kind": "ElementMapping", "origin": "Inferred",
"sourceElementIds": ["ProductLaunchedOn"], "targetElementIds": ["ProductLaunchedOn"] }
]
}
}
The NOT NULL on sku arrived as a minCardinality 1 restriction plus a maxCardinality 1 restriction on ex:Product. Three request details matter here: targetHintSchema is what inferred mapping aligns toward (set "useProjectAsTargetHint": true to use the project's own schema instead, which reads but never writes the project); mapping.kind is inferred, explicit, or ai; and sourceFormat and targetFormat are independent, so owl on either side — or both — is equally valid.
4. Replay — the plan as an artifact
The plan object is not a log. It is the input to plan/execute. Store it next to the source file and the transformation becomes reproducible under review. Note that plan travels as a string, which is exactly what --rawfile gives you.
jq -n --rawfile src product.sql --rawfile plan plans/product.plan.json '{
sourceFormat: "sql",
sourceSchema: $src,
plan: $plan,
targetFormat: "owl"
}' | curl -sS -X POST \
"https://coremodels.example.com/graph/transform/plan/execute/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data @-
The parsed plan passes through the same validation gate as a freshly produced one — a stored plan earns no shortcut — and then executes. Same plan plus same source produces the same output, and no target hint is needed on replay: the plan already says what to do. The response is the standard envelope with schema.
Failure modes worth recognizing
- A bad format key returns
success: falsewitherrors[0].messagereadingUnknown schema format 'ttl'. Use: jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm. - Missing body keys are rejected before any work:
Body must include 'format' and 'schema'.on import,Body must include 'sourceFormat' and 'sourceSchema'.on the mapping routes, andBody must include 'plan' (the plan JSON produced by a mapping call).on replay. - Turtle syntax problems come back on path
$with a line number, as shown above.
Whatever the route, the reading order is the same: check success; if false, errors[].path and errors[].message pinpoint the problem; if true, read lossiness before you celebrate. A non-empty ledger on a successful call is not a bug — it is the API declining to pretend that OWL and, say, SQL have identical expressive power.
For ready-to-paste bodies covering the other formats on this surface, see the Transform section of the CoreModels documentation.