# JSON Schema — API: Four Routes and a Round Trip: JSON Schema Over HTTP

A couple of format keys in the CoreModels transform surface are honest about being one-way. `odm` decodes only, because entity documentation is authored, not generated. `synapse` encodes only, and says so out loud when you try the other direction: *"'synapse' is encode-only: a Synapse schema is plain draft-07 JSON Schema - decode it with the 'jsonschema' format."*

# Four Routes and a Round Trip: JSON Schema Over HTTP

A couple of format keys in the CoreModels transform surface are honest about being one-way. `odm` decodes only, because entity documentation is authored, not generated. `synapse` encodes only, and says so out loud when you try the other direction: *"'synapse' is encode-only: a Synapse schema is plain draft-07 JSON Schema - decode it with the 'jsonschema' format."*

`jsonschema` is not one of those. It appears in both lists the dispatcher publishes:

```
decode: jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm
encode: jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | synapse
```

It reads JSON Schema and it writes JSON Schema, with the same coder on both sides, and `sia` is an alias for the same key in both directions. This article walks the four routes that matter for it: export, import, map, and replay.

Everything below assumes a host of `https://coremodels.example.com` (substitute yours), a bearer token in `$TOKEN`, and a 32-hex project id in `$PROJECT_ID`. Transform routes carry no `api/` prefix, every route is authorized and project-scoped, and every response uses one envelope: `success`, `lossiness`, `errors`, and a payload key.

| Purpose | Route | Role |
|---|---|---|
| Export the project's schema | `POST graph/transform/schema/export/{projectId}` | Viewer |
| Import a schema into the project | `POST graph/transform/schema/import/{projectId}` | Admin |
| Map source onto a target (stateless) | `POST graph/transform/schema/map/{projectId}` | Viewer (`ai`: Editor) |
| Map onto the project's own schema | `POST graph/transform/schema/mapImport/{projectId}` | Admin (dry-run: Viewer; `ai`: Editor) |
| Replay a stored plan (stateless) | `POST graph/transform/plan/execute/{projectId}` | Viewer |

## Export: the project as a JSON Schema document

The smallest useful body on the whole surface:

```bash
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": "jsonschema" }'
```

The response carries the document under `schema`, as a JSON object:

```json
{
  "success": true,
  "lossiness": [],
  "errors": [],
  "schema": {
    "type": "object",
    "properties": {
      "customer_key": { "type": "integer" },
      "full_name": {
        "type": "string",
        "x-sia-role": "identifier",
        "x-sia-priority": 1,
        "x-maps-to": { "schema.org": "https://schema.org/name" }
      },
      "signup_date": { "type": "string" },
      "orders": { "type": "array", "items": { "$ref": "#/$defs/Order" } }
    },
    "required": ["customer_key", "full_name"],
    "x-maps-to": { "schema.org": "https://schema.org/Person" },
    "$defs": {
      "Order": {
        "type": "object",
        "properties": {
          "order_id": { "type": "string" },
          "status": { "enum": ["open", "shipped"] }
        },
        "required": ["order_id"]
      }
    }
  }
}
```

Three rules are visible there, and all three matter before you diff an export against a hand-written file.

**One type becomes the root document; the rest go under `$defs`.** A reference between them is emitted as `{"$ref": "#/$defs/Order"}`. When the schema was originally imported from JSON Schema, the coder remembers which type was the root and restores it; otherwise the first type takes that seat.

**Property names come from element labels, not from node ids.** CoreModels node ids must be alphanumeric, so `customer_key` lives internally under a camelCase id while the label - the name your source used - is what gets emitted.

**Meaning rides along.** Cross-standard assertions come back as `x-maps-to`, and the annotation keywords `x-sia-role`, `x-sia-priority`, and `x-sia-instruction` come back where they were set. That is the same carrier the LinkML, OWL, and vendor exports read, which is why an equivalence recorded once shows up everywhere.

Note `signup_date`: a governed date-time exports as `{"type": "string"}`. The `format: "date-time"` keyword is preserved when it arrived from a JSON Schema source - unmodeled keywords ride verbatim - but when the definition came from elsewhere there is nothing to restore. An empty `lossiness` array means the exporter found a JSON Schema home for everything it was asked to write.

## Import: a schema into the project

Import is the same envelope with the arrow reversed: `format` plus `schema`, where `schema` is the document as a **string**.

```bash
curl -sS -X POST \
  "https://coremodels.example.com/graph/transform/schema/import/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "format": "jsonschema",
        "schema": "{ \"$id\": \"Person\", \"type\": \"object\", \"title\": \"Person\", \"properties\": { \"name\": { \"type\": \"string\" }, \"age\": { \"type\": \"integer\" }, \"active\": { \"type\": \"boolean\" } }, \"required\": [\"name\"] }"
      }'
```

```json
{ "success": true, "lossiness": [], "errors": [], "projectId": "…" }
```

This route requires **Admin** and it writes: object becomes Type, property becomes Element, `enum` becomes a controlled list, `$ref` becomes a reference, and `allOf: [{ "$ref": … }, { … }]` becomes inheritance plus the inline properties. An optional `spaces` array targets specific spaces; omitting it uses the project's main space. A body missing either field is refused before anything is touched, with `Body must include 'format' and 'schema'.`

## Map: aligning one JSON Schema onto another

`schema/map` is the stateless engine route - the project scopes authorization and nothing else. It takes a source, a target format, an optional target hint, and a `mapping` strategy. The example below is the realistic case: a legacy `Order` schema carrying a field the canonical model no longer has, aligned onto the canonical shape through the `x-maps-to` annotations both sides already carry.

```bash
curl -sS -X POST \
  "https://coremodels.example.com/graph/transform/schema/map/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @- <<'JSON'
{
  "sourceFormat": "jsonschema",
  "sourceSchema": "{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"title\":\"Order\",\"type\":\"object\",\"x-maps-to\":{\"schema.org\":\"https://schema.org/Order\"},\"properties\":{\"orderId\":{\"type\":\"string\",\"x-maps-to\":{\"schema.org\":\"https://schema.org/orderNumber\"}},\"placedAt\":{\"type\":\"string\",\"format\":\"date-time\",\"x-maps-to\":{\"schema.org\":\"https://schema.org/orderDate\"}},\"status\":{\"enum\":[\"open\",\"shipped\",\"closed\"],\"x-maps-to\":{\"schema.org\":\"https://schema.org/orderStatus\"}},\"legacyRegionCode\":{\"type\":\"string\"}},\"required\":[\"orderId\"]}",
  "targetFormat": "jsonschema",
  "targetHintFormat": "jsonschema",
  "targetHintSchema": "{\"title\":\"Order\",\"type\":\"object\",\"x-maps-to\":{\"schema.org\":\"https://schema.org/Order\"},\"properties\":{\"orderId\":{\"type\":\"string\",\"x-maps-to\":{\"schema.org\":\"https://schema.org/orderNumber\"}},\"placedAt\":{\"type\":\"string\",\"format\":\"date-time\",\"x-maps-to\":{\"schema.org\":\"https://schema.org/orderDate\"}},\"status\":{\"enum\":[\"open\",\"shipped\",\"closed\"],\"x-maps-to\":{\"schema.org\":\"https://schema.org/orderStatus\"}}},\"required\":[\"orderId\"]}",
  "mapping": {
    "kind": "explicit",
    "guide": "{\"autoMatchByMapsTo\":true,\"taxonomyDirectives\":[{\"sourceTaxonomyId\":\"Order::status::enum\",\"targetTaxonomyId\":\"Order::status::enum\",\"treatment\":\"InlineEnum\"}],\"drops\":[\"Order::legacyRegionCode\"]}"
  }
}
JSON
```

The legacy field is gone, the controlled list survived, and the removal is on the record rather than in your memory:

```json
{
  "success": true,
  "lossiness": [
    {
      "kind": "StructuralDrop",
      "path": "Element[Order::legacyRegionCode]",
      "explanation": "Dropped by the SIA guide."
    }
  ],
  "errors": [],
  "schema": {
    "type": "object",
    "properties": {
      "orderId": { "type": "string", "x-maps-to": { "schema.org": "https://schema.org/orderNumber" } },
      "placedAt": { "type": "string", "x-maps-to": { "schema.org": "https://schema.org/orderDate" }, "format": "date-time" },
      "status": { "enum": ["open", "shipped", "closed"], "x-maps-to": { "schema.org": "https://schema.org/orderStatus" } }
    },
    "required": ["orderId"],
    "x-maps-to": { "schema.org": "https://schema.org/Order" },
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "title": "Order"
  }
}
```

(A drop's ledger record is declared on the plan operation itself, and both the validation gate and the executor report declared lossiness, so the raw response lists this entry twice - one fact, stated twice.)

The response also carries `plan` - six operations here: the drop, a taxonomy mapping, a type mapping, and three element mappings, each stamped `"origin": "Explicit"`. Nothing was written to the project, so every `schema/map` call is inherently a dry run. To write the aligned result into CoreModels instead, `schema/mapImport` takes the same `mapping` object, uses the project's current schema as the target, and honors `dryRun: true` so you can read the ledger before committing.

Two details save time here. The guide is strict on purpose - an unknown key is refused with its path, as in `guide.fieldMapings: Unknown mapping-guide key 'fieldMapings'. Known keys: autoMatchByMapsTo, fieldMappings, taxonomyDirectives, drops.` - because a silently ignored typo would execute a plan you did not intend. And `autoMatchByMapsTo` aligns exactly the elements that carry a matching `x-maps-to` on both sides; anything else needs an entry of its own, or it leaves the target with a `StructuralDrop` record saying so.

## Replay: the plan as the artifact

Take the `plan` object from that response, stringify it, and post it to `plan/execute` with the same source:

```bash
jq -n --rawfile source order.json --rawfile plan plan.json \
  '{ sourceFormat: "jsonschema", sourceSchema: $source,
     plan: $plan, targetFormat: "jsonschema" }' > replay.json

curl -sS -X POST \
  "https://coremodels.example.com/graph/transform/plan/execute/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @replay.json
```

`plan` is a **string** containing the plan JSON - that trips people up exactly once. The response is the familiar `{ success, lossiness, errors, schema }`.

Two properties make this worth doing rather than decorative. The same plan and the same source produce the same output, byte for byte. And a stored plan earns no shortcut: it passes through the identical validation gate as a freshly produced one, so an edited plan is judged on its merits rather than trusted because it arrived in a file. An unknown operation kind comes back as `plan.operations[0].kind: Unknown operation kind 'Rename'. Use: TypeMapping | ElementMapping | TaxonomyMapping | RelationMapping | ComponentMapping | Drop.`

## Errors worth recognizing

- `$: The root of a JSON Schema must be an object.` - the payload parsed as JSON but was a string, number, or array.
- `Could not parse the 'jsonschema' schema: …` - it was not valid JSON at all.
- `Unknown schema format 'json-schema'. Use: …` - the key is `jsonschema` (or `sia`).
- `The schema has no root type to encode.` - a JSON Schema document needs one root object; this shows up when a mapping renamed the type the source had marked as its root.
- `errors` mentioning `Referenced taxonomy '…::enum' does not resolve.` - an `enum` reached the target without a `taxonomyDirective` to carry it.

Read `lossiness` on every call, including the successful ones. `success: true` means the call ran, not that nothing changed - the ledger is the difference between a conversion you can defend and one you merely executed. For the full route table, role requirements, and ready-to-paste bodies for the other formats, see the schema transformation guide in the CoreModels documentation.
