# ODCS — Quickstart: One Contract, Three Schemas: An ODCS Quickstart

A data contract is a promise with a schema inside it. The ODCS documents your platform team publishes - Open Data Contract Standard, the Bitol project's v3 line - carry tables and columns, but also servers, quality checks, SLAs, ownership: everything the promise needs and most target formats cannot hold. This quickstart takes one real contract through a single CoreModels call three times - to JSON Schema, to SQL, and back to ODCS - and shows you how to read what each conversion kept, what it set aside, and where the set-aside parts went.

# One Contract, Three Schemas: An ODCS Quickstart

A data contract is a promise with a schema inside it. The ODCS documents your platform team
publishes - Open Data Contract Standard, the Bitol project's v3 line - carry tables and columns,
but also servers, quality checks, SLAs, ownership: everything the promise needs and most target
formats cannot hold. This quickstart takes one real contract through a single CoreModels call three
times - to JSON Schema, to SQL, and back to ODCS - and shows you how to read what each conversion
kept, what it set aside, and where the set-aside parts went.

The endpoint is stateless: it decodes, maps, encodes, and returns everything in one response.
Nothing is written anywhere. You need the API host (`https://coremodels.example.com` throughout), a
bearer token in `$TOKEN`, a project id in `$PROJECT_ID` (32 hex characters; it scopes authorization
only, and Viewer is enough), plus `curl` and `jq`.

## The contract

Save this as `orders.odcs.yaml`. It is a compact but honest v3.1.0 contract: a head with identity
and purpose, one schema object with six properties, a primary key, a physical type, a quality
check, and a `servers` block - the parts of ODCS that behave differently from a plain schema.

```yaml
apiVersion: v3.1.0
kind: DataContract
id: urn:datacontract:sales:orders
name: orders
version: 1.2.0
status: active
domain: sales
description:
  purpose: Governed view of confirmed customer orders.
  usage: Analytics and settlement reporting.
servers:
  - server: analytics-pg
    type: postgres
    host: db.internal.example
    port: 5432
schema:
  - name: orders
    physicalName: orders_v1
    physicalType: table
    logicalType: object
    description: One row per confirmed order.
    properties:
      - name: order_id
        logicalType: string
        physicalType: uuid
        primaryKey: true
        primaryKeyPosition: 1
        required: true
        unique: true
      - name: customer_ref
        logicalType: string
        required: true
      - name: order_total
        logicalType: number
        physicalType: numeric(12,2)
        required: true
        quality:
          - metric: nullValues
            mustBe: 0
      - name: placed_at
        logicalType: timestamp
        required: true
      - name: item_count
        logicalType: integer
      - name: gift
        logicalType: boolean
```

The format key is `odcs`, and it works in both directions: CoreModels decodes ODCS and encodes
ODCS, so the same key is valid as a source and as a target. (Not every format is like that - `odm`
decodes only, `synapse` encodes only.)

## The call

The default mapping strategy, `inferred`, matches the source against a target hint and declines
rather than guesses when there is none. For a pure format conversion, pass the source as its own
hint; that yields an identity plan and lets the target coder do the format work. The contract
travels inside JSON as a string, so build the body with `jq --rawfile`:

```bash
jq -n --rawfile s orders.odcs.yaml '{
  sourceFormat: "odcs",
  sourceSchema: $s,
  targetFormat: "jsonschema",
  targetHintFormat: "odcs",
  targetHintSchema: $s,
  mapping: { kind: "inferred" }
}' > request.json

curl -sS -X POST \
  "https://coremodels.example.com/graph/transform/schema/map/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @request.json > result.json
```

The response is one envelope: `success`, the produced `schema`, the executed `plan`, and the
`lossiness` ledger.

## Reading the schema

`jq '.schema' result.json` - for a JSON-shaped target it is a JSON object:

```json
{
  "type": "object",
  "properties": {
    "order_id": { "type": "string" },
    "customer_ref": { "type": "string" },
    "order_total": { "type": "number" },
    "placed_at": { "type": "string" },
    "item_count": { "type": "integer" },
    "gift": { "type": "boolean" }
  },
  "required": ["order_id", "customer_ref", "order_total", "placed_at"]
}
```

Each ODCS property became a JSON Schema property, its `logicalType` mapped across: `string` to
`string`, `number` to `number`, `integer` to `integer`, `boolean` to `boolean`, `timestamp` to a
string-carried date-time. The contract's own `required: true` flags became the `required` array -
ODCS declares requiredness explicitly per property, and that declaration is what travels. Notice
what is *not* here: no `uuid`, no `numeric(12,2)`, no primary key, no quality check. Physical
facts and contract governance have no JSON Schema slots. Where did they go? That is the ledger's
job.

## Reading the plan

`jq '.plan.operations | length' result.json` returns 7 - one type mapping and six element
mappings. The first:

```json
{
  "kind": "TypeMapping",
  "origin": "Inferred",
  "sourceTypeId": "orders",
  "targetTypeId": "orders",
  "targetLabel": "orders"
}
```

`origin: "Inferred"` means the match came from labels and types - a heuristic, marked as one. The
element ids are worth a glance too: `ordersOrderId`, `ordersPlacedAt`. CoreModels ids are
camelCase alphanumerics composed from the object and property names, while the original
`order_id`-style names are kept as labels and re-emitted on output. Keep the plan: it is a
replayable artifact, and `POST /graph/transform/plan/execute/{projectId}` will run it again
against the same source for the same result.

## Reading the ledger

`jq '.lossiness | unique' result.json` - the raw list holds four records here, because the target
hint is the same contract and is decoded through the same path, so its two entries appear again;
`unique` collapses the duplicated pair:

```json
[
  { "kind": "SemanticNarrowing", "path": "$",
    "explanation": "Contract section(s) the IR does not model structurally not modeled by the IR: servers; preserved verbatim under odcs.raw.* for exact re-emit." },
  { "kind": "SemanticNarrowing", "path": "schema[0].properties[2].quality",
    "explanation": "Quality checks are not structurally modeled by the IR; they were preserved verbatim under odcs.quality for exact re-emit." }
]
```

Here is the single most useful thing to understand about converting data contracts: **a perfectly
clean contract still produces ledger entries**, because a contract carries more than a schema. The
`servers` block and the quality check on `order_total` have no structural home in the neutral
model the engine works over, so each is preserved verbatim in a format-specific channel and
declared. Nothing was dropped silently; nothing was invented. `success: true` means "it ran" -
the ledger is the part you actually review. The four kinds you will meet are `StructuralDrop`,
`TypeApproximation`, `ConstraintRelaxation`, and `SemanticNarrowing`.

## Second target: SQL

Change two keys in the request - `targetFormat: "sql"`, add `vendor: "postgres"` - and run the
same call. The `schema` field is now a string of DDL:

```sql
CREATE TABLE "orders" (
  "order_id" VARCHAR(255) NOT NULL,
  "customer_ref" VARCHAR(255) NOT NULL,
  "order_total" NUMERIC NOT NULL,
  "placed_at" TIMESTAMP NOT NULL,
  "item_count" INTEGER,
  "gift" BOOLEAN
);
```

Requiredness became `NOT NULL`; logical types drove the column types. Look closely at
`order_total`: the contract says `physicalType: numeric(12,2)`, but the column is plain `NUMERIC`.
The DDL is generated from the *logical* type - `number` - because the physical annotation is an
ODCS fact riding the preservation channel, not a portable type. The same ledger entries appear
again, and the quality check (`nullValues mustBe 0`) is one of them: the generated table enforces
`NOT NULL`, but the contract's quality rule is not a SQL constraint here, and the ledger is your
prompt to wire it into whatever checks that database runs.

## Third target: ODCS itself

Now set `targetFormat: "odcs"` and run once more. This is the round trip, and it is where the
preservation channel pays out:

```yaml
apiVersion: v3.1.0
kind: DataContract
id: urn:datacontract:sales:orders
name: orders
version: 1.2.0
status: active
domain: sales
description:
  purpose: Governed view of confirmed customer orders.
  usage: Analytics and settlement reporting.
schema:
  - name: orders
    logicalType: object
    physicalName: orders_v1
    physicalType: table
    description: One row per confirmed order.
    properties:
      - name: order_id
        logicalType: string
        physicalType: uuid
        required: true
        unique: true
        primaryKey: true
        primaryKeyPosition: 1
      - name: customer_ref
        logicalType: string
        required: true
      - name: order_total
        logicalType: number
        physicalType: numeric(12,2)
        required: true
        quality: [{"metric":"nullValues","mustBe":0}]
      - name: placed_at
        logicalType: timestamp
        required: true
      - name: item_count
        logicalType: integer
      - name: gift
        logicalType: boolean
servers: [{"server":"analytics-pg","type":"postgres","host":"db.internal.example","port":5432}]
```

Everything is back: the contract head with its id, version, and status; the physical names and
types; the primary-key markers; the quality check; the `servers` block, re-emitted in JSON flow
style (JSON is a YAML subset, so this is still a valid v3.1.0 contract). Key order and quoting are
deterministic - run the call five times and you get one distinct output - and feeding this output
back in reproduces it exactly. The ledger still shows the same entries, because they describe
how the trip was made, not a failure of it.

## Where to go next

If the contract should become a governed CoreModels model rather than a converted file, `POST
/graph/transform/schema/import/{projectId}` with `{ "format": "odcs", "schema": "<the YAML>" }`
writes its objects and properties into a project - that route writes, so it needs Admin. If an AI
agent should do this instead of a script, the same engine is exposed over MCP as the
`transform_schema` tool with the same argument names (the `mapping` object flattens to a
`mappingKind` argument). The transform section of the CoreModels documentation lists every
endpoint, role, and format key.
