The Ossie HTTP Surface: Import, Export, Map, Replay
Four verbs cover everything you will do with an Apache Ossie semantic model through the CoreModels API, and picking the right one is mostly a question of *where the schema lives*. Is it a file you have? Import it, or map it statelessly. Is it a governed model in a CoreModels project? Export it. Do you need the same conversion again next month, byte for byte? Replay a stored plan.
The Ossie HTTP Surface: Import, Export, Map, Replay
Four verbs cover everything you will do with an Apache Ossie semantic model through the CoreModels API, and picking the right one is mostly a question of where the schema lives. Is it a file you have? Import it, or map it statelessly. Is it a governed model in a CoreModels project? Export it. Do you need the same conversion again next month, byte for byte? Replay a stored plan.
This is the endpoint-level reference for the osi and osi-json format keys: real request bodies, real responses, and the direction limits stated plainly.
Directions, stated honestly
Both Ossie keys work in both directions. osi and osi-json appear in the decode list (jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm) and in the encode list (jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | synapse). Two neighbors in those lists are one-way, and it is worth knowing why yours is not: odm decodes only (ODM entities are authored documentation, not a generated artifact) and synapse encodes only (its output is plain draft-07 JSON Schema, so you re-import it as jsonschema).
For Ossie the asymmetry is smaller and lives in the serialization, not the direction:
| Key | Decoding | Encoding |
|---|---|---|
osi | YAML or JSON — one parse reads either | YAML (spec version 0.1.1 when the model carries none of its own) |
osi-json | YAML or JSON — identical to osi | JSON — the serialization dbt ingests |
Legacy documents stamped version: "1.0" — a release that never existed — are accepted and normalized to 0.1.1, with a lossiness record saying so.
The routes
All paths sit under https://coremodels.example.com/graph/transform/... with no api/ prefix, all require authorization, all are project-scoped.
| Purpose | Route | Role |
|---|---|---|
| Import a model into a project | POST graph/transform/schema/import/{projectId} | Admin |
| Export a project's schema | POST graph/transform/schema/export/{projectId} | Viewer |
| Map source → target, stateless | POST graph/transform/schema/map/{projectId} | Viewer (ai: Editor) |
| Map onto the project's schema | POST graph/transform/schema/mapImport/{projectId} | Admin (dry-run: Viewer) |
| Replay a stored plan, stateless | POST graph/transform/plan/execute/{projectId} | Viewer |
Every response uses the same envelope: success (it ran — not "nothing changed"), lossiness (the honest change list), errors (populated only on failure), plus a payload key — projectId, schema, summary, and plan where the route produces one.
1. Import: model → project
Take a two-dataset model with a relationship:
version: "0.1.1"
semantic_model:
- name: crm
datasets:
- name: customers
source: crm.customers
primary_key: customer_id
fields:
- name: customer_id
- name: full_name
- name: signup_date
dimension:
is_time: true
- name: subscriptions
source: crm.subscriptions
primary_key: subscription_id
fields:
- name: subscription_id
- name: subscriber_id
- name: plan_code
relationships:
- name: subscription_customer
from: subscriptions
to: customers
from_columns: [subscriber_id]
to_columns: [customer_id]
jq -n --rawfile schema crm.yaml '{ format: "osi", schema: $schema }' \
| curl -s "https://coremodels.example.com/graph/transform/schema/import/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @-
The body takes format, schema, and an optional spaces array (empty means the project's main space). The response aggregates the read-side and write-side ledgers:
{
"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": "StructuralDrop",
"path": "Relation[subscriptionCustomer]",
"explanation": "IR relation instances are not written by the CoreModels encoder; the relation was dropped."
}
],
"errors": [],
"projectId": "3f2a9c7e5b1d4a8fbc0e6d2a9f4b7c31"
}
Two records, two different authors. The first is the Ossie decoder telling you the format has no type system. The second is the CoreModels writer telling you that dataset relationships have no carrier on the graph side; a model's metrics produce the sibling record IR components (curated views) are not written by the CoreModels encoder; the component was dropped.
What did land: a Type per dataset (customers, subscriptions), an Element per field, labels preserved exactly as written (customer_id, signup_date), ids camel-cased for the graph's id index (customersCustomerId), signup_date typed DateTime because of its time dimension, and primary-key fields marked required — requiredness round-trips through the transform element-facts mixin.
2. Export: project → model
curl -s "https://coremodels.example.com/graph/transform/schema/export/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "format": "osi" }'
Swap "osi" for "osi-json" to get the JSON serialization of the identical model. For a project titled Customer Domain holding one Customer type with three elements, the emitted document is:
version: "0.1.1"
semantic_model:
- name: "Customer Domain"
datasets:
- name: "Customer"
source: "Customer"
fields:
- name: "customer_key"
expression:
dialects:
- dialect: "ANSI_SQL"
expression: "customer_key"
- name: "full_name"
expression:
dialects:
- dialect: "ANSI_SQL"
expression: "full_name"
- name: "signup_date"
expression:
dialects:
- dialect: "ANSI_SQL"
expression: "signup_date"
dimension:
is_time: true
Read that carefully, because three encoder rules are visible in it. The semantic model's name is the project title. Each dataset needs a source and each field needs an expression per the spec, so when the model in hand carries neither, we derive them from the names rather than emit an invalid document. And there is no primary_key: the project stores types, elements, their value types and requiredness — not the format-specific Ossie annotations. Those annotations (source, unique keys, per-dialect expressions, ai_context, custom extensions) survive intact through the stateless routes below, where the document never leaves the request.
3. Map: source → target with a hint
schema/map is the engine route — decode, plan, gate, execute, encode — and it writes nothing. Request fields: sourceFormat, sourceSchema, targetFormat, optional vendor (SQL output), optional targetHintFormat + targetHintSchema, optional useProjectAsTargetHint, and the mapping object (kind: inferred | explicit | ai; guide; caseInsensitive).
Converting the same crm.yaml to LinkML, with the source doubling as its own hint:
jq -n --rawfile schema crm.yaml '{
sourceFormat: "osi",
sourceSchema: $schema,
targetFormat: "linkml",
targetHintFormat: "osi",
targetHintSchema: $schema,
mapping: { kind: "inferred", caseInsensitive: true }
}' | curl -s "https://coremodels.example.com/graph/transform/schema/map/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @-
The schema string in the response unescapes to:
id: https://coremodels.example.com/ns/crm
name: crm
prefixes:
linkml: https://w3id.org/linkml/
imports:
- linkml:types
default_range: string
classes:
customers:
attributes:
customer_id:
required: true
full_name:
signup_date:
range: datetime
subscriptions:
attributes:
subscription_id:
required: true
subscriber_id:
plan_code:
…and the ledger carries the familiar type approximation — twice, because the source and the inline hint each decode once — plus one more entry:
{
"kind": "StructuralDrop",
"path": "Relation[subscriptionCustomer]",
"explanation": "Source relation not carried by any relation-mapping operation."
}
That is the engine being precise rather than clever. An inferred plan aligns types, elements, and taxonomies by label; it never invents relation operations. If you need the relationship carried, the plan is an editable artifact — add a RelationMapping operation naming the relation and the osiRelationship group, and replay it. Metrics work the same way through ComponentMapping.
Two other limits are worth internalizing before you automate this route. Inference requires a hint (useProjectAsTargetHint: true uses the project's own schema instead of an inline one), and label matching is one-to-one: if two datasets in one model share a field name, the gate rejects the plan with Target Element id 'customersCustomerId' is produced by 2 operations. Warehouse-style prefixed columns (ws_item_sk, i_item_sk) never hit that; an unprefixed customer_id in two datasets does.
If you want the mapped result written into a project rather than returned, schema/mapImport takes sourceFormat, sourceSchema, mapping, optional spaces, and dryRun. Run it with dryRun: true first: you get the plan, a summary of types/elements/taxonomies/components/relations, and the ledger — before anything is written.
4. Replay: the plan as an artifact
Store the plan object from any mapping response and hand it back later. Note that the plan field on this route is the plan JSON as a string:
jq -n --rawfile schema crm.yaml --rawfile plan plan.json '{
sourceFormat: "osi",
sourceSchema: $schema,
plan: $plan,
targetFormat: "osi-json"
}' | curl -s "https://coremodels.example.com/graph/transform/plan/execute/$PROJECT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @-
A stored plan earns no shortcut: it passes the identical validation gate before it executes. Same plan plus same source gives the same output — we have checked byte-identical results across target formats, which is what makes this route safe to put in CI.
When it fails
Failures come back inside the envelope, not as surprises. Unknown schema format '...' lists the valid keys. A broken document returns The document is not valid YAML: ... with the parser's complaint; valid YAML that is not a semantic model returns The document contains no semantic_model entry.; an empty payload returns The OSI YAML document is empty. On the mapping side, explicit without a guide and ai without a server-configured Anthropic key are both declined with a message naming the fix — and ai additionally requires Editor or Admin membership, because it spends a paid budget on every call.
The full endpoint reference, including the data (records) routes that share this envelope, is in the CoreModels transform docs.