Open Semantic Interchange logoDeep dive

Every Key, Every Record: How the Ossie Coder Maps to the IR

An Apache Ossie semantic model and a typed intermediate representation disagree about the world in one fundamental way: an Ossie field has no type. It is a name, an optional expression, an optional description, and optional dimension metadata. The IR that CoreModels (by ARAMAI) moves schemas through is typed, and so is every neighboring format — SQL, Avro, JSON Schema, LinkML.

Every Key, Every Record: How the Ossie Coder Maps to the IR

An Apache Ossie semantic model and a typed intermediate representation disagree about the world in one fundamental way: an Ossie field has no type. It is a name, an optional expression, an optional description, and optional dimension metadata. The IR that CoreModels (by ARAMAI) moves schemas through is typed, and so is every neighboring format — SQL, Avro, JSON Schema, LinkML.

Everything interesting about this coder follows from that mismatch, and from one rule: anything the target cannot hold goes into the ledger — never silently discarded, never smuggled into the wire format. Here is the complete map.

The structural map

Ossie constructIR constructNotes
the documentone IR schemaid is the first model's sanitized name; label is the name as written
first semantic_model entrythe schema node itselfits description and ai_context ride on the schema's annotations
further semantic_model entriesflattened into the same schemaids prefixed with the model name; recorded in the ledger
datasetTypelabel = name as written; id = sanitized name
fieldElementString — or DateTime when dimension.is_time
primary_key membershipRequired on the Elementplus a marker annotation so the key can be rebuilt on encode
relationshipRelation in the osiRelationship groupone custom group, labeled OSI Relationship, Type → Type endpoints
metricComponentmembers are the datasets its expression names
custom_extensionsschema annotationsread from the semantic model and, tolerantly, from the document root

Ids are sanitized because CoreModels node ids must be alphanumeric — the id index tokenizes on separators, so a raw web_sales could not be matched by an exact-id query. web_sales becomes webSales, ws_item_sk under it becomes webSalesWsItemSk, and the original names survive untouched as labels. Collisions get a numeric suffix: two datasets named web_sales and web sales decode to webSales and webSales2.

Metric membership is a best-effort scan: each dataset's name or source is matched at word boundaries inside the metric's expression (preferring ANSI_SQL), so SUM(web_sales.ws_sales_price) attaches the metric to webSales. When nothing matches, the component is still created — with a ledger entry saying it references no types.

The annotation contract

Everything the IR has no first-class slot for rides in dotted osi.* annotations, which is what makes an exact re-emit possible:

KeyCarries
osi.versionthe wire version string
osi.descriptiondescription text (schema, dataset, field, metric)
osi.aiContext.instructions / .synonyms / .examplesthe ai_context block (arrays as JSON)
osi.sourcea dataset's source
osi.uniqueKeysunique_keys, verbatim as JSON
osi.primaryKey"true" when the field is in the primary key
osi.expression.<dialect>one entry per dialect, expression text verbatim
osi.fromColumns / osi.toColumnsa relationship's join columns
osi.namea relationship's original name
osi.metric"true" — marks a component as an Ossie metric
osi.ext.<vendor_name>a custom extension's data
osi.raw.<key>any unknown wire key, preserved verbatim as JSON

Decoding the two-dataset retail model we test against produces exactly this on the schema node:

{
  "osi.version": "0.2.0.dev0",
  "osi.description": "Retail sales semantic model.",
  "osi.aiContext.instructions": "Prefer web_sales for revenue questions.",
  "osi.aiContext.examples": "[\"What was total revenue last month?\"]",
  "osi.ext.ARAMAI": "{\"owner\":\"analytics-team\",\"refresh\":\"daily\"}"
}

…and on a primary-key field with an expression:

{
  "osi.expression.ANSI_SQL": "ws_item_sk",
  "osi.description": "Item surrogate key.",
  "osi.primaryKey": "true"
}

The relationship keeps its own: osi.name, osi.fromColumns, osi.toColumns. The metric keeps osi.metric, osi.expression.ANSI_SQL, osi.description.

Note the version: 0.2.0.dev0 is preserved as written. We emit 0.1.1 when a model carries no version — pinned against the spec's JSON schema at the osi-0.1.1-rc1 tag of the apache/ossie repository — but we never rewrite a version somebody chose. The one exception is the legacy 1.0, a release that never existed; it normalizes to 0.1.1 with a SemanticNarrowing record, in both directions.

Parsing posture

The document is parsed once into a generic object graph and walked defensively — never bound to rigid classes, so an unfamiliar key cannot break a decode. JSON is a YAML subset, so the same parse reads the osi-json serialization; the two format keys differ only in what they emit. Consequences:

  • An unknown key on a dataset, field, relationship, or metric is preserved as osi.raw.<key> and re-emitted verbatim. Give a dataset refresh_cron: "0 3 * * *" and it comes back out as refresh_cron: "0 3 * * *".
  • An unknown key on the semantic model itself is reported (Unrecognized semantic-model keys not modelled: owner_team.) rather than carried.
  • Unknown keys inside ai_context, expression, and dimension are reported the same way.
  • primary_key accepts a scalar or a list. expression accepts the spec's dialects structure or a bare string, which is treated as ANSI_SQL.
  • semantic_model accepts a single mapping as well as a list.

Three inputs fail outright: an empty document (The OSI YAML document is empty.), invalid YAML (The document is not valid YAML: ...), and a valid document with no model (The document contains no semantic_model entry.).

The lossiness inventory

Decoding can emit five records:

KindWhen
TypeApproximation at $always, when the model has fields: OSI carries no field type system; every field was decoded as String (or DateTime when dimension.is_time). One summarized record for the whole schema, not one per field.
SemanticNarrowing at versionthe legacy 1.0 was normalized to 0.1.1
SemanticNarrowing at semantic_modelseveral models flattened into one schema with prefixed ids
StructuralDrop at ...primary_keya key column matching no field: they are dropped and a re-encode will emit the key without them
StructuralDrop at ...relationships[i]endpoints that resolve to no dataset: the relation was dropped

Plus SemanticNarrowing for a metric whose expression names no known dataset, for unrecognized keys in the places listed above, and for a custom extension with no vendor_name.

Encoding reports what Ossie cannot express:

KindPathReason
ConstraintRelaxationTaxonomy[...]OSI has no controlled-list construct; the taxonomy (and any enum constraint) is not represented
SemanticNarrowingType[...]OSI has no inheritance; the parent link is not represented
SemanticNarrowingRelation[...]only dataset-to-dataset OSI relationships are expressible; other relation groups are not
StructuralDropComponent[...]only components marked osi.metric are exported

That last table is why a JSON Schema enum converts to Ossie with a ConstraintRelaxation: the values have nowhere to live.

Two encode-side losses carry no record, because the wire format has no slot to lose them from. Field types are one: an Ossie field has none, and the decode-side approximation already states that rule for the format. Requiredness that is not key membership is the other: primary_key is rebuilt only from elements marked as key members, so a required flag arriving from JSON Schema or SQL does not invent a key. Both are worth remembering when you convert into Ossie.

What round-trips

Decode → encode → decode is stable. For the retail model, the structural signature — types, their element lists, each element's label, value type and requiredness, relations with their group and endpoints, components with their members — is identical before and after. So is the signature after a round trip through the JSON serialization, and the re-emitted YAML carries ai_context, unique_keys, the composite primary_key, per-dialect expressions, the time dimension, and the custom extension (abridged below to one dataset and two of its fields; the item dataset the relationship points at and the second metric are omitted for length):

version: "0.2.0.dev0"
semantic_model:
  - name: "retail_analytics"
    description: "Retail sales semantic model."
    ai_context:
      instructions: "Prefer web_sales for revenue questions."
      examples: ["What was total revenue last month?"]
    datasets:
      - name: "web_sales"
        source: "tpcds.web_sales"
        description: "Web channel sales facts."
        primary_key: ["ws_item_sk","ws_order_number"]
        unique_keys: [["ws_item_sk","ws_order_number"]]
        ai_context:
          synonyms: ["online sales","web orders"]
        fields:
          - name: "ws_item_sk"
            description: "Item surrogate key."
            expression:
              dialects:
                - dialect: "ANSI_SQL"
                  expression: "ws_item_sk"
          - name: "ws_sold_date"
            description: "Date of sale."
            expression:
              dialects:
                - dialect: "ANSI_SQL"
                  expression: "ws_sold_date"
            dimension:
              is_time: true
    relationships:
      - name: "web_sales_item"
        from: "web_sales"
        to: "item"
        from_columns: ["ws_item_sk"]
        to_columns: ["i_item_sk"]
    metrics:
      - name: "total_revenue"
        description: "Total revenue across web sales."
        expression:
          dialects:
            - dialect: "ANSI_SQL"
              expression: "SUM(web_sales.ws_sales_price)"
        ai_context:
          synonyms: ["revenue"]
    custom_extensions:
      - vendor_name: "ARAMAI"
        data: "{\"owner\":\"analytics-team\",\"refresh\":\"daily\"}"

Four details in that document are decisions, not accidents:

Emission is hand-written. Fixed key order, two-space indent, quoted scalars, JSON flow style for lists — output is byte-stable and exactly re-readable. The JSON serialization derives from the same emission, and scalar style disambiguates typing there: quoted scalars stay strings, plain ones re-type, so is_time becomes a real JSON true.

Extensions are emitted under the semantic model, their home in the spec, even when the input placed them at the document root; data is written as a JSON string literal, because the spec types that field as a string.

Every field gets an expression. The spec requires one, so a field that arrived without any dialect is re-emitted with its own name as the ANSI_SQL expression. Likewise a dataset with no source is emitted with its sanitized name — web_sales gets source: "webSales" — rather than producing a document that fails spec validation.

primary_key is rebuilt from the per-field markers. That is why a key column matching no field is a StructuralDrop at decode time: it would quietly change the key's composition on the way back out.

Fidelity beyond the coder

Two boundaries beyond the coder change what survives — both visible in the ledger.

Through the mapping engine (the schema/map route and the transform_schema tool), an inferred plan carries types, elements, and taxonomies. Relationships come back as StructuralDrop at Relation[...]source relation not carried by any relation-mapping operation — until the plan carries a RelationMapping operation naming the osiRelationship group; metrics need a ComponentMapping. Add those two operations to the returned plan, replay it through plan/execute, and the relationships and metrics blocks come back.

Through a CoreModels project, the graph stores types, elements, value types and requiredness — not the format-specific osi.* annotations, and not relation instances or components (both reported as StructuralDrop on import). An export from a project is therefore a clean 0.1.1 document: real dataset and field names, sources and expressions derived from those names, time dimensions intact, no ai_context. If you need an Ossie document to survive a conversion with its annotations whole, keep it in the stateless routes, where the document never leaves the request.

The endpoint reference and the per-format quickstarts are in the CoreModels transform docs.