# ShEx — API: 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:

```jsonc
{
  "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

```bash
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

```bash
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):

```text
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:

```json
{
  "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:

```jsonc
{
  "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:

```json
{
  "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.
- `401` means 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.
