The ShEx HTTP Surface: Import, Export, Map, and Replay
Five HTTP endpoints carry ShEx through CoreModels, and they are the same five that carry every other schema format we support — nothing about ShEx is a special case on the wire. What changes is one string: the format key `shex`. This article is the contract reference for that surface: exact routes, request and response bodies, roles, options, and the honest statement of what each direction can and cannot do.
The ShEx HTTP Surface: Import, Export, Map, and Replay
Five HTTP endpoints carry ShEx through CoreModels, and they are the same five that carry every other schema format we support — nothing about ShEx is a special case on the wire. What changes is one string: the format key shex. This article is the contract reference for that surface: exact routes, request and response bodies, roles, options, and the honest statement of what each direction can and cannot do.
First, the direction truth, because we state it per format and some formats are one-way: ShEx is fully bidirectional. It appears in both of our dispatch lists:
- Decode (import side):
jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm - Encode (export side):
jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | synapse
By contrast, odm is decode-only and synapse is encode-only — if you ever hit those walls with other formats, the error message says so explicitly. With shex you will not: shapes go in, shapes come out. The payload is always the ShExC (compact syntax) document as a plain string; there are no ShEx-specific request options (the vendor key applies only to sql output, the synapse* keys only to synapse output).
All routes live under https://coremodels.example.com/graph/transform/..., are authenticated with Authorization: Bearer $TOKEN, and are project-scoped by a 32-character hex {projectId} in the path.
| Purpose | Route | Role |
|---|---|---|
| Import ShEx into a project | POST /graph/transform/schema/import/{projectId} | Admin |
| Export a project as ShEx | POST /graph/transform/schema/export/{projectId} | Viewer |
| Map ShEx onto a target, stateless | POST /graph/transform/schema/map/{projectId} | Viewer (ai mapping: Editor) |
| Map ShEx onto the project's schema | POST /graph/transform/schema/mapImport/{projectId} | Admin (dry-run: Viewer; ai mapping: Editor) |
| Replay a stored plan | POST /graph/transform/plan/execute/{projectId} | Viewer |
Every response, success or failure, uses one envelope:
{
"success": true, // false only if the call could not proceed
"lossiness": [ // what changed but did not stop the run
{ "kind": "TypeApproximation", "path": "Element[...]", "explanation": "..." }
],
"errors": [], // on failure: [{ "path": "...", "message": "..." }]
"schema": "..." // or "projectId" / "summary", per endpoint; mapping responses add "plan"
}
success: true means "it ran," never "nothing changed." The lossiness ledger is the change report; its four kinds are StructuralDrop, TypeApproximation, ConstraintRelaxation, and SemanticNarrowing.
1. schema/import — ShEx into a project
curl -s -X POST "https://coremodels.example.com/graph/transform/schema/import/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "format": "shex", "schema": "PREFIX schema: <https://schema.org/>\n<PersonShape> {\n schema:name xsd:string ;\n schema:age xsd:integer ?\n}" }'
Body keys: format, schema (the ShExC text), and optionally spaces (target space ids; empty means the project's main space). Response: { "success": true, "lossiness": [], "errors": [], "projectId": "..." }. Shapes become Types, triple constraints become Elements, inline value sets become Taxonomies, EXTENDS becomes inheritance — and because ShEx predicates are IRIs, each one is lifted into the element's mapsTo annotation automatically.
2. schema/export — a project as ShEx
curl -s -X POST "https://coremodels.example.com/graph/transform/schema/export/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "format": "shex" }'
The schema field of the response is the ShExC document as a string. Representative output for a project holding a Customer model (four elements, a status controlled list, and a mapsTo of https://schema.org/name on the full-name element):
PREFIX cm: <https://coremodels.example.com/ns/>
PREFIX schema: <https://schema.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
cm:CustomerShape {
cm:customer_key xsd:integer ;
schema:name xsd:string ;
cm:signup_date xsd:dateTime ? ;
cm:status [ "draft" "active" "closed" ] ? ;
}
Worth reading closely, because the encoder's rules are visible here. A model that was born in ShEx re-exports with its original prefixes, shape names, predicates, and xsd: datatypes — the coder preserves them for exact re-emission. A model that arrived from elsewhere (SQL, in this example) gets deterministic synthesis instead: shape names become cm:<PascalCase>Shape under our cm: namespace, and — the good part — any element carrying a mapsTo URI gets that URI reversed back into a prefixed predicate. That is why the full-name column exports as schema:name, a real schema.org property any RDF toolchain understands, while unmapped columns export under cm:. Required elements carry no cardinality symbol (ShEx's "exactly one"); optional ones get ?; collections get + or *. Taxonomies come back as inline value sets.
3. schema/map — stateless, with a target hint
schema/map is the mapping engine: decode the source, produce a plan, validate it through the universal gate, execute deterministically, encode the target. Nothing is written to the project — every call is inherently a dry run. Inferred mapping matches by label and type against a target hint, so supply one:
{
"sourceFormat": "shex",
"sourceSchema": "PREFIX schema: <https://schema.org/>\nPREFIX xsd: <http://www.w3.org/2001/XMLSchema#>\n\nschema:BookShape {\n schema:name xsd:string ;\n schema:isbn xsd:string ;\n schema:datePublished xsd:dateTime ? ;\n}",
"targetFormat": "jsonschema",
"targetHintFormat": "jsonschema",
"targetHintSchema": "{ \"$id\": \"Book\", \"type\": \"object\", \"title\": \"Book\", \"properties\": { \"name\": { \"type\": \"string\" }, \"isbn\": { \"type\": \"string\" }, \"datePublished\": { \"type\": \"string\", \"format\": \"date-time\" } } }",
"mapping": { "kind": "inferred", "caseInsensitive": true }
}
POST that to /graph/transform/schema/map/$PROJECT_ID with the same headers. The ShEx shape label Book (the Shape suffix is stripped on decode) and the element labels name, isbn, datePublished line up with the hint's title and properties, so the inferred strategy aligns them one-to-one. The response carries two artifacts:
{
"success": true,
"lossiness": [ ... ],
"errors": [],
"schema": { ... }, // the produced JSON Schema
"plan": { "operations": [ ... ] } // the EXECUTED plan — store this
}
Alternatives to inferred: explicit (you author a mapping guide; unknown guide keys are rejected with a path-carrying error) and ai (a server-side proposal validated by the identical gate — requires Editor/Admin membership and a server-configured key, and is declined honestly when the key is absent). You can also set useProjectAsTargetHint: true to map toward the project's own schema instead of an inline hint.
4. plan/execute — replay
The plan is a replayable artifact. Feed it back — note that plan travels as a string of plan JSON, not an object:
{
"sourceFormat": "shex",
"sourceSchema": "PREFIX schema: <https://schema.org/>\nPREFIX xsd: <http://www.w3.org/2001/XMLSchema#>\n\nschema:BookShape {\n schema:name xsd:string ;\n schema:isbn xsd:string ;\n schema:datePublished xsd:dateTime ?\n}",
"plan": "{ \"operations\": [ ... ] }",
"targetFormat": "jsonschema"
}
POST to /graph/transform/plan/execute/$PROJECT_ID. The stored plan earns no shortcut: it passes the same validation gate as a freshly produced one, then executes deterministically — same plan plus same source produces the same output. The response returns schema and the aggregated lossiness.
Failure modes, stated plainly
- Unknown format key on import:
Unknown schema format '...'. Use: jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm.(the export-side message lists the encode set instead). - Malformed ShExC:
Could not parse the 'shex' schema: ...with the underlying reason. - An empty or whitespace-only document fails cleanly at request validation, before the decoder runs:
errors: [{ "path": "request", "message": "Body must include 'format' and 'schema'." }](the mapping endpoints name their own keys, e.g.'sourceFormat' and 'sourceSchema'). The decoder's own guard —The ShExC document is empty.at path$— surfaces on surfaces that reach it directly, such as the MCP tool. 401means a missing or expired bearer token, or a role below the table above.
A failed call sets success: false and populates errors; a successful call can still carry a non-empty lossiness list, and that is by design. Read it every time.
For request bodies covering the other twelve formats and the data-plane endpoints, see the transform section of the CoreModels docs.