Azure Synapse logoDeep dive

Inside the synapse Coder: The Whitelist, the Ledger, and the Edge Cases

The entire CoreModels `synapse` profile can be stated in one sentence: *encode the neutral model as JSON Schema, then keep only what the Synapse `JsonSchema` REST object has a field for — and write down every single thing that rule removes.* This deep dive unpacks that sentence into the parts an integrator eventually needs: the IR-to-output mapping, the exact keyword whitelist and how strips are classified, the extras channel that carries verbatim keywords through the neutral model, the complete inventory of lossiness records the encoder can emit, and the edge cases our test suite pins — including the ones where we deliberately refuse to guess.

Inside the synapse Coder: The Whitelist, the Ledger, and the Edge Cases

The entire CoreModels synapse profile can be stated in one sentence: encode the neutral model as JSON Schema, then keep only what the Synapse JsonSchema REST object has a field for — and write down every single thing that rule removes. This deep dive unpacks that sentence into the parts an integrator eventually needs: the IR-to-output mapping, the exact keyword whitelist and how strips are classified, the extras channel that carries verbatim keywords through the neutral model, the complete inventory of lossiness records the encoder can emit, and the edge cases our test suite pins — including the ones where we deliberately refuse to guess.

Grounding, stated up front: the profile is built against the public Synapse REST documentation (the JsonSchema object's field set is the authoritative subset) and live publicly registered schemas — sage.annotations-experimentalData.specimenID-0.0.1 and SynapseDocs-Color, the latter captured verbatim in mid-2026 and used as a shape fixture in our tests. Where the public record does not settle a behavior, the coder records the uncertainty instead of inventing an answer. And the direction is singular by design: synapse encodes only; its output is plain draft-07, decoded — when you need the reverse — by the jsonschema format.

The pipeline in one paragraph

The Synapse encode is a down-conversion of our standard JSON Schema export: the neutral model (IR) goes through the same encoder that produces the jsonschema output, bracketed by ordered passes that apply the Synapse conventions — date-time preparation before the delegated encode, then IR-level lossiness recording, the $defsdefinitions rename, the recursive whitelist scrub, and the identity pass that stamps $schema and the registered $id first, the keyword order the public fixtures use.

IR construct → Synapse output

IR constructSynapse outputNotes
Root typeThe root object schema (type, properties, required, title)title from the root type's label when the source carried none
Non-root typeAn entry under definitionsNever $defs — the rename rewrites the container and every $ref pointer
Type with a parentallOf: $ref to the parent in definitions + inline member with own propertiesVerified by test: #/definitions/CreativeWork, no $defs anywhere
ElementA property under properties; Required collects into the required arrayLabels are the property names
String / RichText primitive"type": "string"RichText's approximation is recorded by the underlying JSON Schema layer
Integer / Double / Boolean"type": "integer" / "number" / "boolean"
DateTime (single-valued)"type": "string" with "format": "date-time"Plus a TypeApproximation record — see the inventory below
DateTime (collection)Array of plain strings, no formatA per-item format cannot be placed; recorded, not faked
Taxonomy referenceenum of term ids, with "type": "string" added when all members are stringsThe type+enum pairing matches the public SynapseDocs-Color schema
Taxonomy term hierarchyFlattened — a draft-07 enum is a flat listThe hierarchy survives only in the curation manifest's value-set Parent column
Collection cardinality"type": "array" with items, minItems, maxItemsAll three are fields of the Synapse object
Type reference"$ref": "#/definitions/{TypeId}"
NullableNot representedRecorded as SemanticNarrowing; see "what we refuse to guess"
ComponentsNothingOne StructuralDrop naming the count
RelationsNothingOne StructuralDrop naming the count

The whitelist, exactly

A keyword survives the scrub if and only if it is a field of the Synapse JsonSchema REST object. The full supported set:

$schema  $id  $ref  type  items  properties  title  description
allOf  anyOf  oneOf  not  format  definitions  enum  const  source
required  maxLength  minLength  maxItems  minItems  uniqueItems  pattern
if  then  else  maximum  minimum  default  contains  additionalProperties

Everything else is removed — and how the removal is reported depends on what the keyword did. Keywords that carried a validation rule are classified as ConstraintRelaxation, because their removal loosens what the schema enforces:

multipleOf  exclusiveMinimum  exclusiveMaximum  maxProperties  minProperties
patternProperties  dependencies  dependentRequired  dependentSchemas
propertyNames  additionalItems  prefixItems  unevaluatedProperties
unevaluatedItems  minContains  maxContains

Any other stripped keyword — annotations like $comment, examples, readOnly, writeOnly, contentMediaType, vendor extensions, unknowns — is a StructuralDrop. One record per strip, each carrying the exact JSON path (#/properties/vialCount/multipleOf), which makes CI allowlists of acknowledged strips practical. A dedicated test walks every key of the encoded output and asserts no unsupported keyword survives anywhere.

The scrub recurses through every position a subschema can occupy: single-subschema keywords (items, not, if, then, else, contains, additionalProperties), subschema arrays (allOf, anyOf, oneOf), and name→subschema maps (properties, definitions).

The extras namespace: how foreign keywords travel

The neutral model carries an annotations bag on every node with an open Extras map — the channel through which keywords the IR does not model structurally travel verbatim. When our jsonschema decoder reads a source, structurally consumed keywords (type, properties, required, items, enum, $ref, allOf, minItems, maxItems, $defs, definitions) are rebuilt on encode; everything elsepattern, minimum, $schema, title, a stray x-vendor-thing — is preserved in Extras and re-emitted as-is. The synapse scrub then runs over the re-emitted result, which yields a clean division of labor: preservation is the neutral layer's job, subsetting is the Synapse layer's job, and the ledger sits between them.

Three consequences worth knowing:

  • The SIA annotation vocabulary is stripped here. Our own semantic keywords (x-sia-role, x-sia-priority, x-sia-instruction, x-maps-to) appear in the standard jsonschema export but are not fields of the Synapse object, so they strip like anything else — each with a StructuralDrop record. A test asserts exactly this for x-sia-role.
  • Coder-namespaced extras never leak. Extras keyed with a dotted namespace by other format coders are excluded from JSON Schema emission entirely, so they never even reach the scrub.
  • Combinator payloads are preserved verbatim — and scrubbed recursively. anyOf/oneOf/if/then/else/not bodies the IR cannot model structurally travel through Extras as raw JSON, and the scrub still descends into them — including the strangest position draft-07 allows, tuple-form items.

That last case is worth seeing concretely, because it is the one our tests guard hardest. A property carrying this preserved payload:

{
  "anyOf": [
    {
      "type": "array",
      "items": [
        { "type": "string", "multipleOf": 3, "$ref": "#/$defs/X" }
      ]
    }
  ]
}

encodes as:

{
  "anyOf": [
    {
      "type": "array",
      "items": [
        { "type": "string", "$ref": "#/definitions/X" }
      ]
    }
  ]
}

with one ConstraintRelaxation at #/properties/poly/anyOf/0/items/0/multipleOf. Each member of a tuple-form items array is a schema position: the walk enters it, strips with correctly indexed paths, and rewrites nested refs. Without that pass, a verbatim payload would be a tunnel under the whitelist.

Identity: the $id grammar and its guards

The registered-schema $id is {base}{org}-{name}-{semver}, where the base is Synapse's production registered-type URI. Because the hyphen delimits the three segments, it cannot appear inside one — so segment inputs are sanitized: ASCII letters, digits, and dots survive; anything else folds to a camelCase boundary (my org-xmyOrgX, blog-postblogPost); leading/trailing dots are trimmed; a fully consumed input falls back to schema. Dots are legal because real organizations and names use them (sage.annotations, experimentalData.specimenID).

The version segment gets its own guard: only plain major.minor.patch is accepted. 1.0.0-rc1 or 0.0.1 beta would corrupt the URI's segment structure, so the encoder substitutes the default 0.0.1 and records a SemanticNarrowing at #/$id naming the rejected version — the call succeeds, the fallback is documented, and a CI gate on that path turns it into a build failure. An empty version is meaningful, not invalid: it emits the unversioned registered-pointer form, the shape the public SynapseDocs-Color schema uses.

Two more identity rewrites, both recorded: a source dialect other than draft-07 is down-converted (SemanticNarrowing at #/$schema), and a source $id that is not already a registered-schema URI is replaced (StructuralDrop at #/$id).

The complete lossiness inventory

Every record this encoder can emit, by trigger:

TriggerKindPath shape
Single DateTime emitted as string + format: date-time (enforcement unconfirmed)TypeApproximationElement[id]
DateTime collection emitted as plain string arrayTypeApproximationElement[id]
Nullable element (no null-union invented)SemanticNarrowingElement[id]
Taxonomy with parented terms flattened to a flat enumStructuralDropTaxonomy[id]
Component projections (no representation)StructuralDropschema.components
Relations (no representation)StructuralDropschema.relations
Stripped constraint keywordConstraintRelaxation#/…/keyword
Stripped annotation/unknown keywordStructuralDrop#/…/keyword
type array narrowed to its single non-null memberSemanticNarrowing#/…/type
type array with multiple non-null members droppedConstraintRelaxation#/…/type
Non-draft-07 dialect down-convertedSemanticNarrowing#/$schema
Source $id replaced by the registered URIStructuralDrop#/$id
Non-semver version option replaced by the defaultSemanticNarrowing#/$id

The type-array rows deserve a note: Synapse documents type as a single value, never an array, so ["string", "null"] (arriving via a preserved combinator payload) narrows to "string" with a record — while ["string", "integer"] has no single member that preserves the union, so the keyword is dropped and the loss classified as a relaxation.

What we refuse to guess

Four behaviors are handled conservatively pending real platform fixtures rather than encoded on speculation: whether Synapse accepts an anyOf null-union for nullable fields (Nullable is narrowed with a lossiness record, and explicit nulls will not validate — our test asserts no anyOf is invented); whether format is enforced as an assertion or treated as a draft-07 annotation (every date-time emit carries a record saying so); cross-schema $ref resolution (the structural encode produces only internal #/definitions/... references); and non-string enum handling (the type: "string" companion is added only when every member is a string, matching the one public fixture we can verify). An honest subset encoder must be honest about the boundary of its own knowledge, too.

Round-trip edge cases

Calling decode on the synapse format fails fast, with the error pointing at jsonschema — asserted by test, mirrored by the API and MCP dispatch. The output being plain draft-07 means the round trip is real, just asymmetric: re-decode via jsonschema and the surviving keywords ride the extras channel back into the IR intact. What is gone is gone — the strips are recoverable only from the ledger you kept. And by construction a second encode of a re-decoded output is quiet: the dialect already matches draft-07, the $id already carries the registered base, the enums already carry their type — the strip list has nothing left to say. Convergence on a fixed point, with the journey fully accounted for in the first run's ledger: that is the profile's contract.