Turtle In, JSON Schema Out: Your First OWL Transform
There is a `.ttl` file somewhere in your repository. Someone modeled the domain properly once — classes, subclass axioms, cardinality restrictions, a SKOS scheme for the controlled list — and then everyone else carried on hand-writing JSON Schema and DDL, because nothing bridged the two.
Turtle In, JSON Schema Out: Your First OWL Transform
There is a .ttl file somewhere in your repository. Someone modeled the domain properly once — classes, subclass axioms, cardinality restrictions, a SKOS scheme for the controlled list — and then everyone else carried on hand-writing JSON Schema and DDL, because nothing bridged the two.
CoreModels treats OWL as a first-class source, not only an export target. The format key is owl, the payload is a Turtle document as plain text, and one HTTP call turns that ontology into a schema your API team can use — together with a ledger of everything the conversion could not carry exactly.
This is the shortest path from a Turtle file to a first result.
The ontology
Save this as catalog.ttl. It is small on purpose, but it exercises the constructs that matter: a class hierarchy, a required property, a repeatable one, an object property pointing at a controlled vocabulary, and a cross-standard equivalence.
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix schema: <https://schema.org/> .
@prefix ex: <https://example.org/catalog#> .
ex:ProductCatalog a owl:Ontology ;
rdfs:label "Product Catalog" .
ex:Product a owl:Class ;
rdfs:label "Product" ;
rdfs:comment "Anything the catalog can sell." ;
owl:equivalentClass schema:Product ;
rdfs:subClassOf [ a owl:Restriction ; owl:onProperty ex:sku ; owl:minCardinality 1 ] ,
[ a owl:Restriction ; owl:onProperty ex:sku ; owl:maxCardinality 1 ] ,
[ a owl:Restriction ; owl:onProperty ex:listPrice ; owl:maxCardinality 1 ] ,
[ a owl:Restriction ; owl:onProperty ex:availability ; owl:maxCardinality 1 ] .
ex:BundledProduct a owl:Class ;
rdfs:label "Bundled Product" ;
rdfs:subClassOf ex:Product .
ex:sku a owl:DatatypeProperty ;
rdfs:label "sku" ;
rdfs:domain ex:Product ;
rdfs:range xsd:string .
ex:listPrice a owl:DatatypeProperty ;
rdfs:label "listPrice" ;
rdfs:domain ex:Product ;
rdfs:range xsd:decimal .
ex:tag a owl:DatatypeProperty ;
rdfs:label "tag" ;
rdfs:domain ex:Product ;
rdfs:range xsd:string .
ex:availability a owl:ObjectProperty ;
rdfs:label "availability" ;
rdfs:domain ex:Product ;
rdfs:range ex:AvailabilityStatus .
ex:AvailabilityStatus a owl:Class ;
rdfs:label "Availability Status" ;
rdfs:subClassOf skos:Concept .
ex:AvailabilityStatusScheme a skos:ConceptScheme ;
rdfs:label "Availability Status" .
ex:Available a ex:AvailabilityStatus, skos:Concept ;
skos:inScheme ex:AvailabilityStatusScheme ;
skos:prefLabel "Available" .
ex:InStock a ex:AvailabilityStatus, skos:Concept ;
skos:inScheme ex:AvailabilityStatusScheme ;
skos:prefLabel "In stock" ;
skos:broader ex:Available ;
skos:exactMatch schema:InStock .
ex:Discontinued a ex:AvailabilityStatus, skos:Concept ;
skos:inScheme ex:AvailabilityStatusScheme ;
skos:prefLabel "Discontinued" .
What you need
A bearer token and a CoreModels project id. The route we are about to call — graph/transform/schema/map — is stateless: the project scopes authorization only. Nothing is read from the graph, nothing is written to it, a Viewer-role token is enough, and a scratch project is fine. (The same call is available to agents as the transform_schema MCP tool; the arguments match, except that the project id travels as graphProjectId and the nested mapping object flattens to mappingKind / guide / caseInsensitive.)
The call
export TOKEN="…"
export PROJECT_ID="…" # 32-char hex project id
jq -n --rawfile ttl catalog.ttl '{
sourceFormat: "owl",
sourceSchema: $ttl,
targetFormat: "jsonschema",
targetHintFormat: "owl",
targetHintSchema: $ttl,
mapping: { kind: "inferred" }
}' > body.json
curl -sS -X POST \
"https://coremodels.example.com/graph/transform/schema/map/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data @body.json
One thing needs explaining before the output: targetHintSchema. Inferred mapping works by matching labels and types against something, and it declines rather than guessing when it has nothing to aim at. Passing the source document as its own hint is the identity case — it says "re-express this faithfully, do not re-align it." Later, when you have a canonical model to map toward, you swap that hint out and the same call becomes an alignment.
The output
{
"success": true,
"lossiness": [],
"errors": [],
"schema": {
"type": "object",
"properties": {
"sku": {
"type": "string",
"x-maps-to": { "ex": "https://example.org/catalog#sku" }
},
"listPrice": {
"type": "number",
"x-maps-to": { "ex": "https://example.org/catalog#listPrice" }
},
"tag": {
"type": "array",
"items": { "type": "string" },
"x-maps-to": { "ex": "https://example.org/catalog#tag" }
},
"availability": {
"enum": ["ex:Available", "ex:InStock", "ex:Discontinued"],
"x-maps-to": { "ex": "https://example.org/catalog#availability" }
}
},
"required": ["sku"],
"x-maps-to": {
"ex": "https://example.org/catalog#Product",
"owl": "https://schema.org/Product"
},
"$defs": {
"ex:BundledProduct": {
"allOf": [
{ "$ref": "#/$defs/ex:Product" },
{ "type": "object", "properties": {} }
],
"x-maps-to": { "ex": "https://example.org/catalog#BundledProduct" }
}
}
},
"plan": { "operations": [ /* the executed plan — see below */ ] }
}
Read the output
Six things in that JSON came from OWL constructs, and each is worth checking back against the Turtle:
required: ["sku"]came fromowl:minCardinality 1onex:sku. Nothing else in the file said "required" — cardinality restrictions are OWL's own carrier for that fact, and the coder reads them as exactly that.skuis a scalar,tagis an array.ex:skucarries amaxCardinality 1restriction;ex:tagcarries no restriction at all. An unrestricted RDF property is optional and multi-valued — the open-world default — and the decoder respects it instead of quietly assuming "one string." Iftagshould be single-valued, say so with a restriction; the difference is now visible instead of implied.listPricebecame"type": "number".xsd:decimalhas no exact JSON Schema counterpart; the numeric family collapses.availabilitybecame anenum. The class declaredrdfs:subClassOf skos:Concept, so it and its individuals were read as a controlled vocabulary rather than as another entity type.x-maps-tois everywhere. Every OWL entity is an IRI, so identity lifts straight through: the entity's own IRI plus theowl:equivalentClass schema:Productassertion both land on theProducttype. The alignment work you did in Turtle survives into JSON Schema instead of stopping at the border.ex:BundledProductarrived as anallOfcomposition in$defs.rdfs:subClassOfbetween two named classes is inheritance, and inheritance is modeled natively rather than flattened.
Read the ledger
"lossiness": [] on this run. That is the exception, not the rule — and the ledger, not success, is the field you should be reading on every call.
success: true means "it ran." It does not mean "nothing changed." The lossiness ledger is the honest list of what the target format could not hold exactly. It has four kinds: StructuralDrop (no home in the target), TypeApproximation (a close-but-not-exact type), ConstraintRelaxation (a rule that could not be enforced), and SemanticNarrowing (meaning narrowed or guessed).
Change one word in the request — "targetFormat": "linkml" — and the same ontology comes back with an entry:
{
"kind": "SemanticNarrowing",
"path": "Taxonomy[ex:AvailabilityStatus]",
"explanation": "permissible_values are flat; the term hierarchy was flattened."
}
That is the skos:broader link from ex:InStock up to ex:Available. LinkML enumerations have no hierarchy, so the parent-child relationship did not survive the trip. Nothing failed; you were told. Treat the ledger as a review checklist: an empty list is a clean conversion, and every entry names the exact path to double-check on the target side.
If the call fails
Two failures account for almost every first attempt, and both name their cause precisely.
If the Turtle is structurally broken — an unclosed IRI, an unterminated literal — the run stops and the envelope carries a line number you can jump to:
{
"success": false,
"lossiness": [],
"errors": [ { "path": "$", "message": "Line 1: Unclosed IRI reference (missing '>')." } ],
"schema": null
}
If you dropped the two targetHint* keys, the engine declines rather than guessing, and says so: The inference resolver requires a target IR to match against. Put the hint back — the source document itself is a perfectly good one until you have a real target.
Note what is not on that list. Dublin Core dates, versioning statements, annotation properties, anything else outside the schema vocabulary: those never fail an import. The decoder counts the triples it did not consume and reports one summarized ledger entry, so a real-world ontology full of metadata still goes through.
What actually happened
Five stages, in order. The Turtle was decoded into a neutral schema model. The inferred strategy produced a plan. The plan went through a validation gate that every plan passes, whoever or whatever produced it. The engine executed it. The result was encoded into JSON Schema. Lossiness is collected at every stage and aggregated into the single ledger you just read.
The plan object in the response is not decoration. It is the executed plan as a reviewable, replayable artifact: the same source plus the same plan produces the same output. Keep it and a one-off conversion becomes a build step.
Next
Swap targetFormat for sql, avro, shex, linkml, protobuf, odcs, osi, jsonld, or owl itself and re-run — the decode work is already done, so a new target costs one word. Then replace targetHintSchema with the schema you actually want to align to, and watch the plan turn from an identity mapping into a real alignment.
For the full endpoint catalog and ready-to-paste bodies for every other format on this surface, see the Transform section of the CoreModels documentation.