Azure Synapse logoAPI

The Synapse Export, Route by Route: A CoreModels API Reference

Every format in the CoreModels transform surface belongs to two lists — the formats we decode and the formats we encode — and `synapse` appears in exactly one of them. The decode list is `jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm`; the encode list swaps `odm` out and `synapse` in. This page is the working reference for that one direction: which routes produce a Synapse-ready schema, what each accepts, which one controls the registered-schema `$id`, and how to bring a Synapse schema back in anyway.

The Synapse Export, Route by Route: A CoreModels API Reference

Every format in the CoreModels transform surface belongs to two lists — the formats we decode and the formats we encode — and synapse appears in exactly one of them. The decode list is jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm; the encode list swaps odm out and synapse in. This page is the working reference for that one direction: which routes produce a Synapse-ready schema, what each accepts, which one controls the registered-schema $id, and how to bring a Synapse schema back in anyway.

We are CoreModels (by ARAMAI); the transform routes live under https://coremodels.example.com/graph/transform/... with no api/ prefix, all [Authorize]d and project-scoped like every other graph API. Every response uses one envelope — success, lossiness, errors, plus the payload key for the route — and success: true means "it ran", never "nothing changed". The lossiness array is the change record.

Route 1: export a governed project — the full-control path

POST graph/transform/schema/export/{projectId} reads the project's schema and encodes it. Viewer role suffices; nothing is written. This is the only REST route that accepts the three Synapse identity options, so it is the one to use when the schema is headed for actual registration:

{
  "format": "synapse",
  "synapseOrg": "myorg.dcc",
  "synapseName": "experimentalData.biopsy",
  "synapseVersion": "0.1.0"
}

The three keys compose the registered-schema $id as {org}-{name}-{semver} under the registered base URI. Their contracts, stated plainly:

  • synapseOrg — your Synapse Organization name. Default example, which is a placeholder: registration requires a pre-created, unique organization, and org.sagebionetworks is reserved for Synapse's own core models.
  • synapseName — the schema-name segment. Default: derived from the project's root type label. Dots are legal (experimentalData.biopsy); anything that cannot live in the segment is folded to a camelCase boundary, because the hyphen is the delimiter between segments.
  • synapseVersion — a plain major.minor.patch. Default 0.1.0-style is not assumed — the default is 0.0.1. Synapse rejects re-registering an existing version, so bump on every change. A version that is not plain semver (1.0.0-rc1, 0.0.1 beta) cannot be expressed in the $id at all; the export falls back to the default and says so in the ledger. An empty string is meaningful: it emits the unversioned registered-pointer form of the $id.

A representative response for a small project:

{
  "success": true,
  "lossiness": [
    {
      "kind": "TypeApproximation",
      "path": "Element[BiopsyCollectedOn]",
      "explanation": "DateTime emitted as 'string' with format 'date-time'; draft-07 treats 'format' as an annotation and Synapse's enforcement is unconfirmed — pending a Sage fixture."
    },
    {
      "kind": "StructuralDrop",
      "path": "Taxonomy[BiopsyStage]",
      "explanation": "Term hierarchy flattened: a draft-07 enum is a flat value list. The hierarchy survives only in the curation manifest's value-set section."
    }
  ],
  "errors": [],
  "schema": {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "$id": "https://repo-prod.prod.sagebase.org/repo/v1/schema/type/registered/myorg.dcc-experimentalData.biopsy-0.1.0",
    "title": "Biopsy",
    "type": "object",
    "properties": {
      "collectedOn": { "type": "string", "format": "date-time" },
      "stage": { "enum": ["stageI", "stageII", "stageIII"], "type": "string" }
    },
    "required": ["stage"]
  }
}

Both ledger entries are worth internalizing. The first is honesty about a behavior we will not invent: draft-07 defines format as an annotation, and whether Synapse enforces date-time as an assertion is not settled by the public documentation, so we emit the keyword and record the uncertainty. The second tells you that a hierarchical vocabulary became a flat enum — and points you at the artifact where the hierarchy does survive, the curation manifest (generated by the generate_synapse_manifests MCP tool, covered in the next article).

Route 2: stateless conversion — any source, defaults on the $id

POST graph/transform/schema/map/{projectId} converts without touching the project (Viewer; the project scopes auth only). Any decodable source format can feed it — here, SQL DDL straight to a Synapse-ready schema:

{
  "sourceFormat": "sql",
  "sourceSchema": "CREATE TABLE Biopsy (\n  sample_id VARCHAR(64) NOT NULL,\n  stage VARCHAR(32),\n  vial_count INTEGER\n);",
  "targetFormat": "synapse",
  "targetHintFormat": "sql",
  "targetHintSchema": "CREATE TABLE Biopsy (\n  sample_id VARCHAR(64) NOT NULL,\n  stage VARCHAR(32),\n  vial_count INTEGER\n);",
  "mapping": { "kind": "inferred" }
}

The hint is required because the default inferred strategy matches labels against a target vocabulary; for a pure conversion, the source is its own hint. The response adds a plan key next to schema and lossiness — the executed mapping as a replayable artifact.

One direction-honesty note that saves a confused afternoon: the mapping routes encode synapse with the default $id segments (example, root-type name, 0.0.1). The synapseOrg/synapseName/synapseVersion keys exist on schema/export and on the transform_schema MCP tool. If a stateless conversion is destined for registration, either re-encode through a project export or drive it over MCP.

Route 3: replay a stored plan

POST graph/transform/plan/execute/{projectId} (Viewer, stateless) re-executes a previously returned plan against the same source — the plan travels as a JSON string:

{
  "sourceFormat": "sql",
  "sourceSchema": "CREATE TABLE Biopsy (\n  sample_id VARCHAR(64) NOT NULL,\n  stage VARCHAR(32),\n  vial_count INTEGER\n);",
  "plan": "{\"operations\":[{\"kind\":\"TypeMapping\",\"origin\":\"Inferred\",\"sourceTypeId\":\"Biopsy\",\"targetTypeId\":\"Biopsy\"}]}",
  "targetFormat": "synapse"
}

A parsed plan is still just a proposal: it passes the identical validation gate every strategy passes, then executes deterministically. Same plan, same source, same output — which is what makes the plan worth storing in the first place. The synapse encode here also uses the default $id segments.

The round trip, honestly

There is no synapse import, by design. Attempting POST graph/transform/schema/import/{projectId} with "format": "synapse" returns:

{
  "success": false,
  "lossiness": [],
  "errors": [
    {
      "path": "format",
      "message": "'synapse' is encode-only: a Synapse schema is plain draft-07 JSON Schema — decode it with the 'jsonschema' format."
    }
  ]
}

The message is the workflow: a registered Synapse schema is valid draft-07 JSON Schema, so import it with the format that reads JSON Schema —

{
  "format": "jsonschema",
  "schema": "{ \"$schema\": \"http://json-schema.org/draft-07/schema#\", \"title\": \"Biopsy\", \"type\": \"object\", \"properties\": { \"stage\": { \"type\": \"string\", \"enum\": [\"stageI\", \"stageII\", \"stageIII\"] } }, \"required\": [\"stage\"] }"
}

— and the round trip closes: project → synapse export → (register, bind, validate in Synapse) → jsonschema import back into a project. The asymmetry of keys is not an asymmetry of capability; it is an accurate description of what a Synapse schema is.

Roles, in one paragraph

Stateless routes (schema/map, plan/execute) and the read-only export need Viewer on the scoping project. Anything that writes to a project (schema/import, and schema/mapImport without dry-run) needs Admin. One override: mapping.kind = "ai" needs Editor or Admin membership even on otherwise-Viewer routes, because it invokes the server-side Anthropic API — and it is declined honestly, in the errors channel, when no server key is configured.

Failure modes worth knowing before they happen

  • Unknown target format — the error lists the encode formats, which is the fastest way to check spelling: jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | synapse.
  • Non-semver synapseVersion — the call succeeds, the $id carries 0.0.1, and a SemanticNarrowing record at #/$id names the version you sent. If your pipeline depends on the version segment, assert on that path (the automation article shows the exact jq gate).
  • Source dialect other than draft-07 — down-converted, recorded at #/$schema; a source document's own $id is replaced and recorded at #/$id.
  • Constraints missing from the output — check lossiness for a ConstraintRelaxation at that exact path before suspecting a bug; the Synapse JsonSchema object simply has no field for keywords like multipleOf, exclusiveMinimum, patternProperties, or dependencies.

That is the whole REST surface for this profile: one route with full identity control, two stateless routes with defaults, an import path that goes through jsonschema, and a ledger on every response that tells you what the subset cost before Synapse tells you the hard way.