# Azure Synapse — Deep 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 `$defs` → `definitions` 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 construct | Synapse output | Notes |
|---|---|---|
| Root type | The root object schema (`type`, `properties`, `required`, `title`) | `title` from the root type's label when the source carried none |
| Non-root type | An entry under `definitions` | Never `$defs` - the rename rewrites the container *and* every `$ref` pointer |
| Type with a parent | `allOf`: `$ref` to the parent in `definitions` + inline member with own properties | Verified by test: `#/definitions/CreativeWork`, no `$defs` anywhere |
| Element | A property under `properties`; `Required` collects into the `required` array | Labels 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 `format` | A per-item `format` cannot be placed; recorded, not faked |
| Taxonomy reference | `enum` of term ids, with `"type": "string"` added when all members are strings | The `type`+`enum` pairing matches the public `SynapseDocs-Color` schema |
| Taxonomy term hierarchy | Flattened - a draft-07 enum is a flat list | The hierarchy survives only in the curation manifest's value-set `Parent` column |
| Collection cardinality | `"type": "array"` with `items`, `minItems`, `maxItems` | All three are fields of the Synapse object |
| Type reference | `"$ref": "#/definitions/{TypeId}"` | |
| `Nullable` | **Not represented** | Recorded as `SemanticNarrowing`; see "what we refuse to guess" |
| Components | Nothing | One `StructuralDrop` naming the count |
| Relations | Nothing | One `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 else* - `pattern`, `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:

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

encodes as:

```json
{
  "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-x` → `myOrgX`, `blog-post` → `blogPost`); 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:

| Trigger | Kind | Path shape |
|---|---|---|
| Single DateTime emitted as string + `format: date-time` (enforcement unconfirmed) | TypeApproximation | `Element[id]` |
| DateTime collection emitted as plain string array | TypeApproximation | `Element[id]` |
| `Nullable` element (no null-union invented) | SemanticNarrowing | `Element[id]` |
| Taxonomy with parented terms flattened to a flat enum | StructuralDrop | `Taxonomy[id]` |
| Component projections (no representation) | StructuralDrop | `schema.components` |
| Relations (no representation) | StructuralDrop | `schema.relations` |
| Stripped constraint keyword | ConstraintRelaxation | `#/…/keyword` |
| Stripped annotation/unknown keyword | StructuralDrop | `#/…/keyword` |
| `type` array narrowed to its single non-null member | SemanticNarrowing | `#/…/type` |
| `type` array with multiple non-null members dropped | ConstraintRelaxation | `#/…/type` |
| Non-draft-07 dialect down-converted | SemanticNarrowing | `#/$schema` |
| Source `$id` replaced by the registered URI | StructuralDrop | `#/$id` |
| Non-semver version option replaced by the default | SemanticNarrowing | `#/$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.
