ODCS logoDeep dive

Anatomy of the ODCS Coder: What Maps, What Rides, What's Declared

Where does `servers:` go?

Anatomy of the ODCS Coder: What Maps, What Rides, What's Declared

Where does servers: go?

It is the right first question for any tool that claims to convert data contracts. An ODCS document is two things braided together: a schema (objects, properties, types, keys) and a contract around it (servers, quality, SLAs, team, pricing). CoreModels converts every format through one neutral intermediate representation (IR), and the IR models schemas — what SQL, Avro, LinkML, and JSON Schema can agree on. So the coder splits three ways: constructs the IR models structurally, facts that ride alongside for exact re-emit, and everything that gets declared in the lossiness ledger. This is the complete anatomy, in that order.

The structural mapping

Import is decode (ODCS YAML or JSON → IR; JSON is a YAML subset, so one parser serves both). Export is encode (IR → ODCS YAML). The spine:

ODCS constructIR constructNotes
the contract documentthe schema nodehead fields ride schema-level extras
schema[] objectType (label = name)ids sanitized to camelCase alphanumerics
propertyElement (id = objectId + PropertyName)label keeps the wire name
logicalType: stringString
logicalType: numberDouble
logicalType: integerInteger
logicalType: booleanBoolean
date / timestampDateTime
timeDateTime + declared approximationthe IR has no time-of-day primitive
required: trueRequiredODCS's own flag is authoritative
logicalType: arraycollection cardinalitybounds from logicalTypeOptions.minItems/maxItems
object property / object array itemschild Type + type referencere-nested on encode
primaryKey, unique, partitionedextras, not structuresee below

Two rows deserve fine print. Names: CoreModels node ids must be alphanumeric, so txn_ref_dt on object tbl becomes element id tblTxnRefDt while the label stays txn_ref_dt and the emitted contract uses the original name; id collisions (two objects both named a) are suffixed (a, a2) rather than merged. Collection bounds: minItems/maxItems become real IR cardinality bounds — they travel into formats that can hold them (JSON Schema output gets "minItems": 1, "maxItems": 100) — and a nonsensical pair (minItems above maxItems) is ignored rather than propagated.

And the opening question: servers — with team, roles, slaProperties, support, price, and any key the coder does not recognize at all — is captured verbatim as JSON under odcs.raw.<key>, declared once in the ledger, and re-emitted exactly when the target is ODCS. A decode never hard-fails on a construct it can preserve.

The extras channel

Every IR node carries an annotation bag; the ODCS coder writes the facts the IR cannot express into dotted odcs.* keys. The namespace, grouped:

KeysWhereWhat they hold
odcs.apiVersion, odcs.id, odcs.name, odcs.version, odcs.status, odcs.domain, odcs.tenant, odcs.dataProductschemathe contract head
odcs.description.purpose / .limitations / .usage, odcs.description.raw.*schemathe description block, unknown description keys verbatim
odcs.stableId, odcs.businessName, odcs.physicalName, odcs.physicalType, odcs.description, odcs.dataGranularityDescriptiontype/elementidentity and physical facts
odcs.logicalType, odcs.logicalTypeOptions, odcs.itemsLogicalType, odcs.items.raw.*element (odcs.logicalType also on type)the verbatim wire types and options
odcs.primaryKey, odcs.primaryKeyPosition, odcs.unique, odcs.partitioned, odcs.partitionKeyPosition, odcs.classification, odcs.encryptedName, odcs.criticalDataElementelementkey, partition, and sensitivity markers
odcs.transformLogic, odcs.transformDescription, odcs.transformSourceObjects, odcs.exampleselementprovenance and samples
odcs.tags, odcs.customProperties, odcs.authoritativeDefinitions, odcs.quality, odcs.relationshipstype/element (at the contract root these keys ride odcs.raw.*)preserved verbatim as JSON
odcs.parenttypemarks a nested type's enclosing object
odcs.raw.*anyany unmodeled wire key, verbatim JSON

The encoder reads all of it back in a fixed order, emitting preserved JSON payloads in YAML flow style — a round-tripped contract shows quality: [{"metric":"rowCount",...}] where the original had block YAML: same data, spec-valid, deterministic. Determinism is total: fixed key order, two-space indent, and a quoting discipline under which a string that would re-parse as a number or boolean gets quoted while varchar(18) stays plain — so encode after the first round trip is a byte-for-byte fixed point, and diffs of generated contracts are meaningful.

primaryKey does not imply required. ODCS carries its own required flag, so the coder believes it: primaryKey: true with required: false decodes as an optional element, odcs.primaryKey riding in extras. (Our OSI import, whose format lacks a separate flag, treats a primary key as required; the ODCS coder deliberately does not.) Convert such a contract to SQL and the key column comes out nullable — the contract said so.

The head, minted or preserved

The spec requires id, version, and status on every contract, so the encoder resolves each head field in order: the preserved wire value if the IR came from ODCS, else a deterministic default — apiVersion: v3.1.0 (the current release), kind: DataContract (the only valid kind), the IR schema's id, version: 1.0.0, status: active. Output is always spec-valid, even from an IR that never saw a contract; an empty model encodes as a head plus schema: []. A contract missing required head fields decodes anyway, with a SemanticNarrowing noting that a re-encode will mint defaults.

apiVersion gets a symmetric policy: a non-v3 version (say v2.2.1) is decoded with v3 semantics and declared; on encode it is replaced with v3.1.0 and declared again. Both records appear on a full trip — the modernization happened in two places, so it is visible twice.

Verbatim types beat mapped types on the way out

For scalars, the encoder prefers the preserved wire logicalType over re-deriving one from the IR primitive:

  • date decodes as DateTime but re-emits as date, not timestamp. Likewise time — an approximation on decode, restored verbatim on encode.
  • An out-of-enum type like geometry decodes as String with a TypeApproximation and re-emits as geometry: textual fidelity wins on the round trip, and the decode-side ledger entry is the record that the IR held it coarsely in between.
  • IR that never saw ODCS gets defaults: Integer → integer, Double → number, Boolean → boolean, DateTime → timestamp, everything else → string (RichText degrades, declared).

Collections and type references override all of that: they always emit logicalType: array and logicalType: object respectively.

Nesting, both directions

An object-typed property, or an array of objects, becomes a child Type marked with odcs.parent — the treatment we use for Protobuf and Avro nested types. Ids compose from the element: tbl.line_items with object items produces child type tblLineItemsItem, whose properties become tblLineItemsItemSku and so on. On encode, a type carrying odcs.parent that some element references is re-nested inside that property rather than emitted top-level — for an array of objects, as an items: block with logicalType: object and the child's properties; for an object-typed property, as inline properties under logicalType: object. Two safety valves: an orphaned nested type (parent marker, nothing referencing it) falls back to top-level emission so it is never dropped; a shared reference (two elements pointing at one type — ODCS has no $ref) is flattened to an inline object with a SemanticNarrowing naming the flattening, while the target also emits as its own schema object.

One round-tripped property shows the whole story — child type re-nested, bounds restored, preserved options in JSON flow style:

- name: line_items
  logicalType: array
  logicalTypeOptions: {"minItems":1,"maxItems":100}
  items:
    logicalType: object
    properties:
      - name: sku
        logicalType: string
        required: true
      - name: quantity
        logicalType: integer

The lossiness inventory

Decode side — everything is a warning, nothing is fatal:

TriggerKindBehavior
unknown key anywhereSemanticNarrowingpreserved under odcs.raw.*, one summarized record per node
quality / relationshipsSemanticNarrowingpreserved verbatim, declared per occurrence
logicalType: timeTypeApproximationDateTime, verbatim type preserved
out-of-enum logicalTypeTypeApproximationString, verbatim type preserved
property with no logicalTypeTypeApproximationString — one summarized record for the whole contract
array with no itemsTypeApproximationitem type approximated as String
array items of type array or out-of-enumTypeApproximationString items, item definition preserved verbatim
object property with no propertiesTypeApproximationString, verbatim logicalType: object preserved
items/properties on a scalar propertySemanticNarrowingpreserved under odcs.raw.*
schema object with non-object logicalTypeSemanticNarrowingpreserved verbatim
non-v3 apiVersionSemanticNarrowingdecoded with v3 semantics
missing required head fieldsSemanticNarrowingre-encode will mint defaults

Encode side — IR constructs ODCS cannot express, declared rather than smuggled:

IR constructKindBehavior
taxonomy (controlled list)ConstraintRelaxationODCS has no enum construct; not represented
element referencing a taxonomyConstraintRelaxationapproximated as string
type inheritanceSemanticNarrowingno inheritance in ODCS
free-standing relationsSemanticNarrowingonly verbatim-preserved odcs.relationships re-emit
components/projectionsStructuralDropno counterpart
element owned by no typeStructuralDropODCS has no free-standing properties
RichText primitiveTypeApproximationdegrades to string
nullable flagConstraintRelaxationrequired models nullability only; null-vs-absent is not representable
unresolvable type referenceTypeApproximationemitted without nested properties (or string items)

The enum story is ODCS's sharpest structural limit, so here it is concretely. A model imported from this table:

CREATE TABLE Customer (
  customer_key INTEGER NOT NULL,
  full_name VARCHAR(255) NOT NULL,
  signup_date TIMESTAMP,
  status ENUM('draft','active','closed')
);

encodes to a contract whose schema body (head omitted) is:

schema:
  - name: Customer
    properties:
      - name: customer_key
        logicalType: integer
        required: true
      - name: full_name
        logicalType: string
        required: true
      - name: signup_date
        logicalType: timestamp
      - name: status
        logicalType: string

with two ConstraintRelaxation entries: one for the taxonomy ("ODCS has no enum/controlled-list construct"), one for the element referencing it. The three allowed values exist in your source, not in the contract — and the ledger names the taxonomy that was relaxed.

Round-trip edge cases, pinned

An itemless array gains items. logicalType: array with no items decodes as a String collection (declared); the re-encode emits an explicit items with logicalType: string. The first pass changes the text; every pass after it is a fixed point.

An array of arrays keeps its text. The IR cannot hold nested collections, so the items approximate as String (declared) — but odcs.itemsLogicalType: array rides along and the re-encode emits logicalType: array items verbatim. Textual fidelity survives a structural approximation.

Rejection is reserved for non-contracts. Empty input; a scalar or sequence at the root; unparseable YAML; a kind other than DataContract; a mapping with none of kind, apiVersion, or schema. These five fail with a path and message; everything else decodes.

The fixed-point invariant, stated precisely. For a contract built from modeled constructs — head, description, physical facts, key markers, quality, relationships, custom properties, tags, nested objects, arrays with bounds, unknown sections — decode → encode reproduces an equivalent spec-valid document, decode → encode → decode yields a structurally identical IR, and one more encode is byte-identical. Our test suite asserts exactly that, and it is what makes the coder safe in a pipeline: the contract is not merely parseable but reconstructible, with every deviation carried as a ledger entry instead of a surprise.

The companion articles cover the HTTP surface, the MCP tool, and pipeline patterns; the transform documentation indexes them all.