From JSON Schema to a Registration-Ready Synapse Schema in One Call
You have a JSON Schema. Synapse — Sage Bionetworks' data platform — will not accept all of it. Registered Synapse schemas are draft-07, and not even all of draft-07: the platform's `JsonSchema` REST object models a specific keyword subset, and anything outside it simply is not part of a registered schema. This quickstart takes a small, real schema across that boundary in one HTTP call to CoreModels (by ARAMAI), then reads the machine-readable ledger of exactly what the boundary cost.
From JSON Schema to a Registration-Ready Synapse Schema in One Call
You have a JSON Schema. Synapse — Sage Bionetworks' data platform — will not accept all of it. Registered Synapse schemas are draft-07, and not even all of draft-07: the platform's JsonSchema REST object models a specific keyword subset, and anything outside it simply is not part of a registered schema. This quickstart takes a small, real schema across that boundary in one HTTP call to CoreModels (by ARAMAI), then reads the machine-readable ledger of exactly what the boundary cost.
One capability statement before anything else, because we hold ourselves to it everywhere: synapse is an encode-only format. CoreModels writes Synapse-ready schemas; it does not read them under the synapse key, because a Synapse schema is plain draft-07 JSON Schema and our jsonschema format already decodes that. You will see both halves of that statement in action below.
What you need
- An API host — we write
https://coremodels.example.comthroughout; substitute your deployment. - A bearer token in
$TOKEN. The call here is stateless and needs only Viewer on the scoping project. - A project id in
$PROJECT_ID. It scopes authorization only; this call writes nothing.
The input
Save this as specimen.json. It is deliberately modern and deliberately a little over the line: a 2020-12 dialect declaration, its own $id, subschemas under $defs, and two constraint keywords (multipleOf, exclusiveMinimum) plus two annotations ($comment, examples) that the Synapse subset does not model.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.org/schemas/specimen",
"title": "Specimen",
"type": "object",
"properties": {
"specimenId": {
"type": "string",
"pattern": "^[A-Z]{2}\\d{4}$",
"minLength": 6,
"$comment": "internal note for authors",
"examples": ["AB1234"]
},
"assay": { "type": "string" },
"collectionDate": { "type": "string", "format": "date-time" },
"vialCount": {
"type": "integer",
"minimum": 0,
"multipleOf": 2,
"exclusiveMinimum": 0
},
"site": { "$ref": "#/$defs/Site" }
},
"required": ["specimenId", "assay"],
"$defs": {
"Site": {
"type": "object",
"properties": {
"siteId": { "type": "string" },
"country": { "type": "string" }
},
"required": ["siteId"]
}
}
}
The call
graph/transform/schema/map is the stateless conversion route: decode the source, run the mapping engine, encode the target. The engine always executes a plan, and the default inferred strategy builds one by matching labels against a target hint — for a pure format conversion, the source document itself is the vocabulary you are mapping toward, so pass it as its own hint:
jq -n --rawfile s specimen.json \
'{sourceFormat:"jsonschema", sourceSchema:$s,
targetFormat:"synapse",
targetHintFormat:"jsonschema", targetHintSchema:$s,
mapping:{kind:"inferred"}}' \
| curl -s "https://coremodels.example.com/graph/transform/schema/map/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @-
(jq -n --rawfile is there purely to turn the schema file into a JSON string field; the request body is ordinary JSON with sourceFormat, sourceSchema, targetFormat, hint, and mapping keys.)
The output
Pipe the response through jq '.schema':
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://repo-prod.prod.sagebase.org/repo/v1/schema/type/registered/example-Specimen-0.0.1",
"type": "object",
"properties": {
"specimenId": {
"type": "string",
"pattern": "^[A-Z]{2}\\d{4}$",
"minLength": 6
},
"assay": { "type": "string" },
"collectionDate": { "type": "string", "format": "date-time" },
"vialCount": { "type": "integer", "minimum": 0 },
"site": { "$ref": "#/definitions/Site" }
},
"required": ["specimenId", "assay"],
"title": "Specimen",
"definitions": {
"Site": {
"type": "object",
"properties": {
"siteId": { "type": "string" },
"country": { "type": "string" }
},
"required": ["siteId"]
}
}
}
Read it top to bottom, because every difference from the input is deliberate:
$schemanow declares draft-07 — the dialect Synapse validates against.$idis the registered-schema URI in the{org}-{name}-{semver}convention. The stateless route composes it from defaults: organizationexample, name derived from the root type (Specimen), version0.0.1. Two honest caveats travel with that:exampleis a placeholder — a real registration needs your own pre-created Synapse Organization, andorg.sagebionetworksis reserved for Synapse's core models. To set all three segments yourself you use the project export route or the MCP tool, both covered later in this series.$defsbecamedefinitions, and the$reftoSitewas rewritten to#/definitions/Siteto match. Synapse's object modelsdefinitions, not the 2020-12 container.pattern,minLength,minimum,maxLength-class constraints survived, because they are fields of the SynapseJsonSchemaobject.multipleOf,exclusiveMinimum,$comment, andexamplesdid not — and that is the half of the story most conversions never tell you.
The ledger
The same response carries a lossiness array. For this input it has six entries:
[
{
"kind": "StructuralDrop",
"path": "#/properties/specimenId/$comment",
"explanation": "'$comment' is not a field of the Synapse JsonSchema object; stripped."
},
{
"kind": "StructuralDrop",
"path": "#/properties/specimenId/examples",
"explanation": "'examples' is not a field of the Synapse JsonSchema object; stripped."
},
{
"kind": "ConstraintRelaxation",
"path": "#/properties/vialCount/multipleOf",
"explanation": "'multipleOf' is not a field of the Synapse JsonSchema object; the constraint was stripped and is no longer enforced."
},
{
"kind": "ConstraintRelaxation",
"path": "#/properties/vialCount/exclusiveMinimum",
"explanation": "'exclusiveMinimum' is not a field of the Synapse JsonSchema object; the constraint was stripped and is no longer enforced."
},
{
"kind": "SemanticNarrowing",
"path": "#/$schema",
"explanation": "Dialect 'https://json-schema.org/draft/2020-12/schema' down-converted to draft-07, the draft Synapse validates against."
},
{
"kind": "StructuralDrop",
"path": "#/$id",
"explanation": "The source $id 'https://example.org/schemas/specimen' was replaced with the Synapse registered-schema URI."
}
]
Notice the classification is not uniform, and that is the point. A dropped $comment is a StructuralDrop — annotation gone, nothing you validated against changed. A dropped multipleOf is a ConstraintRelaxation — your schema now enforces less than you wrote, and if you register it without reading this list, the first odd vial count will be the thing that tells you. The dialect down-convert is a SemanticNarrowing. The fourth kind, TypeApproximation, appears when a value type is represented by a close-but-not-exact one — you will meet it when exporting date-time fields from a governed CoreModels project.
The habit to build on call one: success: true means the call ran. The ledger is what changed. An empty array is a clean conversion; anything else is your pre-registration review checklist, each entry carrying an exact JSON path.
The encode-only half, demonstrated
Try to use synapse as a source format and CoreModels declines with instructions rather than guessing:
{
"success": false,
"lossiness": [],
"errors": [
{
"path": "sourceFormat",
"message": "'synapse' is encode-only: a Synapse schema is plain draft-07 JSON Schema — decode it with the 'jsonschema' format."
}
]
}
That is not a limitation to work around; it is the correct shape of the world. The document you just produced is JSON Schema, so feeding it back in works today with "sourceFormat": "jsonschema" — there is nothing Synapse-specific left to parse, because everything Synapse-specific about the profile lives on the encode side: the subset, the identity convention, and the ledger.
Where this leaves you
In one call you have a draft-07 document shaped for the Synapse JsonSchema object, an explicit registered-schema $id, and a six-line account of the difference between what you wrote and what will be enforced.
Nothing was written to the project along the way. The route is stateless, so you can rerun the call as often as the schema changes while it is still taking shape, and read the ledger fresh each time — Viewer access is all it ever needs. The habit that pays off later is to treat the two outputs as one artifact: the schema you will register and the ledger that says what it no longer enforces belong in the same commit, in front of the same reviewer. Next steps in this series: the full REST surface with roles and the synapseOrg/synapseName/synapseVersion options (t2), the same export driven over MCP together with its companion curation-manifest CSVs (t3), wiring the determinism guarantees into CI (t4), and the complete keyword whitelist and lossiness inventory from inside the coder (t5).