Open Semantic Interchange logoQuickstart

Ten Minutes to Your First Apache Ossie Transform

A semantic model lands in your repository: a YAML file with datasets, fields, a primary key, and a couple of time dimensions. It is the analytics team's description of what the warehouse *means*. Now somebody needs it as a table definition, or as JSON Schema for a validator, or as the JSON serialization dbt ingests. This walkthrough takes you from that file to a converted schema with one HTTP call — and, just as importantly, to a machine-readable ledger of everything the conversion did and did not preserve.

Ten Minutes to Your First Apache Ossie Transform

A semantic model lands in your repository: a YAML file with datasets, fields, a primary key, and a couple of time dimensions. It is the analytics team's description of what the warehouse means. Now somebody needs it as a table definition, or as JSON Schema for a validator, or as the JSON serialization dbt ingests. This walkthrough takes you from that file to a converted schema with one HTTP call — and, just as importantly, to a machine-readable ledger of everything the conversion did and did not preserve.

Apache Ossie (formerly OSI, the Open Semantic Interchange) is the 0.1-line specification for semantic models: datasets, fields with per-dialect expressions, relationships, metrics, and AI context. CoreModels (by ARAMAI) implements it as a transform format in both directions, under two format keys:

  • osi — YAML, the spec's canonical example serialization. This is what we emit by default.
  • osi-json — the same model as JSON, the serialization dbt ingests.

On the way in the two keys are interchangeable: JSON is a YAML subset, so a single parse reads either. On the way out the key picks the serialization. Where a model carries no version of its own we emit spec version 0.1.1, pinned against the spec's JSON schema at the osi-0.1.1-rc1 tag of the apache/ossie repository; a version already on the model is re-emitted as it arrived.

What you need

  • A CoreModels API host. We use https://coremodels.example.com as the placeholder.
  • A bearer token in $TOKEN. The endpoint below is stateless and needs only Viewer on the project.
  • A project id in $PROJECT_ID. The project scopes authorization; this call writes nothing to it.

The transform routes live under graph/transform/... — no api/ prefix.

The document

Save this as storefront.yaml. It is small on purpose, but it exercises the three constructs that decide what a conversion can do: a dataset with a source, a primary key, and a field marked as a time dimension.

version: "0.1.1"
semantic_model:
  - name: storefront
    description: Storefront order analytics.
    datasets:
      - name: orders
        source: analytics.orders
        description: One row per placed order.
        primary_key: order_id
        fields:
          - name: order_id
            description: Surrogate order key.
          - name: ordered_at
            dimension:
              is_time: true
          - name: order_total
          - name: channel

Notice what is not there: field types. An Ossie field is a name, an optional expression, an optional description, and optional dimension metadata. There is no type: integer slot to fill. Hold that thought — the ledger is about to bring it up.

The call

We use POST graph/transform/schema/map/{projectId} — the stateless engine route. It decodes the source, produces a mapping plan, validates that plan through the same gate every strategy passes, executes it deterministically, and encodes the result into the target format. Nothing is written to the project, so every call is inherently a dry run.

One thing to know before you run it: the default mapping strategy is inferred, which matches labels against a target hint and refuses to run without one (The inference resolver requires a target IR to match against.). For a straight format conversion, the source is its own hint — every label matches itself and the plan is an identity mapping. That is what the targetHint* fields do below.

jq -n --rawfile schema storefront.yaml '{
  sourceFormat: "osi",
  sourceSchema: $schema,
  targetFormat: "sql",
  vendor: "postgres",
  targetHintFormat: "osi",
  targetHintSchema: $schema,
  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 @-

vendor picks the SQL dialect: postgres (default), mysql, or sqlserver.

The response

This is the engine's actual output for that input:

{
  "success": true,
  "lossiness": [
    {
      "kind": "TypeApproximation",
      "path": "$",
      "explanation": "OSI carries no field type system; every field was decoded as String (or DateTime when dimension.is_time). One summarized approximation for the whole schema."
    },
    {
      "kind": "TypeApproximation",
      "path": "$",
      "explanation": "OSI carries no field type system; every field was decoded as String (or DateTime when dimension.is_time). One summarized approximation for the whole schema."
    }
  ],
  "errors": [],
  "schema": "CREATE TABLE \"orders\" (\n  \"order_id\" VARCHAR(255) NOT NULL,\n  \"ordered_at\" TIMESTAMP,\n  \"order_total\" VARCHAR(255),\n  \"channel\" VARCHAR(255)\n);\n",
  "plan": {
    "operations": [
      {
        "kind": "TypeMapping",
        "origin": "Inferred",
        "sourceTypeId": "orders",
        "targetTypeId": "orders",
        "targetLabel": "orders"
      },
      {
        "kind": "ElementMapping",
        "origin": "Inferred",
        "sourceElementIds": ["ordersOrderId"],
        "targetElementIds": ["ordersOrderId"]
      },
      {
        "kind": "ElementMapping",
        "origin": "Inferred",
        "sourceElementIds": ["ordersOrderedAt"],
        "targetElementIds": ["ordersOrderedAt"]
      },
      {
        "kind": "ElementMapping",
        "origin": "Inferred",
        "sourceElementIds": ["ordersOrderTotal"],
        "targetElementIds": ["ordersOrderTotal"]
      },
      {
        "kind": "ElementMapping",
        "origin": "Inferred",
        "sourceElementIds": ["ordersChannel"],
        "targetElementIds": ["ordersChannel"]
      }
    ]
  }
}

Unescaped, the schema string is:

CREATE TABLE "orders" (
  "order_id" VARCHAR(255) NOT NULL,
  "ordered_at" TIMESTAMP,
  "order_total" VARCHAR(255),
  "channel" VARCHAR(255)
);

Reading the result

The semantics that Ossie does carry, survived. primary_key: order_id made order_id a required field, which the SQL encoder wrote as NOT NULL. dimension.is_time: true made ordered_at a DateTime, which became TIMESTAMP. Those two are the only typing signals the format has, and both round-tripped.

Ids are sanitized; labels are not. The plan shows sourceTypeId: "orders" alongside targetLabel: "orders", and elements named ordersOrderId. CoreModels node ids must be alphanumeric, so a name like web_sales folds to camelCase webSales for the id while the original name stays on the label — which is why the emitted DDL still quotes the real column names.

The ledger tells you what was guessed. success: true means the call ran, not that nothing changed. The TypeApproximation record is the honest headline for this format: with no type system in the source, order_total and channel land as VARCHAR(255) because nothing in the document says otherwise. We record that once per decode, summarized for the whole schema, instead of emitting one record per field — it appears twice here because this route also decodes the target hint, and our hint is the same document.

Four lossiness kinds show up across every CoreModels format: StructuralDrop (no home in the target), TypeApproximation (close-but-not-exact type), ConstraintRelaxation (a rule that could not be enforced), and SemanticNarrowing (meaning narrowed or guessed). An empty list means a clean conversion; a non-empty list is a review checklist, not an error.

Same call, different target

Change targetFormat and nothing else. With "targetFormat": "osi-json" the same source comes back as the JSON serialization, ready for tooling that ingests Ossie models as .json:

{
  "version": "0.1.1",
  "semantic_model": [
    {
      "name": "storefront",
      "description": "Storefront order analytics.",
      "datasets": [
        {
          "name": "orders",
          "source": "analytics.orders",
          "description": "One row per placed order.",
          "primary_key": ["order_id"],
          "fields": [
            {
              "name": "order_id",
              "description": "Surrogate order key.",
              "expression": {
                "dialects": [{ "dialect": "ANSI_SQL", "expression": "order_id" }]
              }
            },
            {
              "name": "ordered_at",
              "expression": {
                "dialects": [{ "dialect": "ANSI_SQL", "expression": "ordered_at" }]
              },
              "dimension": { "is_time": true }
            },
            {
              "name": "order_total",
              "expression": {
                "dialects": [{ "dialect": "ANSI_SQL", "expression": "order_total" }]
              }
            },
            {
              "name": "channel",
              "expression": {
                "dialects": [{ "dialect": "ANSI_SQL", "expression": "channel" }]
              }
            }
          ]
        }
      ]
    }
  ]
}

Two details worth noticing. The spec requires every field to carry an expression, so fields that arrived without one are re-emitted with their own name as the ANSI_SQL dialect expression. And is_time comes back as a real JSON boolean, not the string "true" — the JSON document carries exactly what the YAML one does.

targetFormat also accepts jsonschema, avro, jsonld, shex, owl, linkml, protobuf, odcs, and synapse. The LinkML rendering of the same model, for instance, keeps required: true on order_id and range: datetime on ordered_at.

Keep the plan

The response carries the executed plan — every operation the engine performed, each stamped origin: "Inferred" so you can see these were heuristic label matches rather than mappings you authored. Store it. Replaying that plan against the same source produces byte-identical output, which turns a one-off conversion into a reviewable pipeline step you can diff in code review.

That is where to go next: the plan as a replayable artifact, batch conversion, and the mapping kinds beyond inferred. The endpoint reference and per-format quickstarts live in the CoreModels transform docs.