Apache Ossie semantic model (ossie-export)
Apache Ossie (incubating) is the ASF's vendor-neutral standard for
exchanging semantic metadata - datasets (tables), fields (columns), relationships (joins)
and metrics - between analytics, BI and AI platforms. An ElastiCORE blueprint already says
everything the dataset side asks for, so ElastiCORE exports it instead of having it declared a second
time. The export follows core spec 0.2.0.dev0 (core-spec/spec.md, ossie-schema.json).
Agents and BI tools that do not know what the data means get, from the same DSL: the joins, what a
code like PLC stands for, and what "revenue" is.
Turning it on
Either in build.gradle - the bundle stays as it is, the export is added:
elasticore {
architecture = 'hexagonal'
rules = ['ossie-export']
}
or with the bundle in a domain's env.yml:
publisher:
rules: [hexagonal-springboot, ossie-export]
Every generation rewrites src/main/resources/blueprint/semantic_model.yml at the blueprint root:
the whole project is one model, so relationships across domains are not cut. It is a sidecar like
schema.yml - always regenerated, never edited by hand. A failed export degrades the export, never
the build. The spec is still a dev version and may change.
What is exported
| DSL | Ossie | Notes |
|---|---|---|
an entity (not @abstract, not @embeddable) | datasets[] | source = the table (@table, or table.prefix + name) |
-- label | description | |
@id fields (composite too) | primary_key | |
@unique, @Index(unique="true") | unique_keys | |
| a value field | fields[] + expression (the column, OSSIE_SQL_2026) + datatype | the type table below |
@formula(sql) | expression = that SQL | an expression in OSSIE_SQL_2026; one with a subquery as a single ANSI_SQL entry - see Dialects |
fields of @extend(Parent) | the child dataset's fields | inherited columns are in that table too |
an @Embedded value object | its columns (deliveryAddress.city) | @embedded(prefix) gives prefix_city |
| an enum field | datatype (String for a text code or the name, Integer for an ordinal) + ai_context: "Codes: PLC = Placed, ..." | the table holds only the code |
| a reference to another entity | relationships[] + the join column field | the real join column |
@password, @writeonly | not exported | values the application never reads back; listed in omitted_columns |
states:, @access, @owner, @event | custom_extensions (ELASTICORE) | Ossie has no words for them |
@compute, @snapshot, money / quantity | the field's custom_extensions | derivation rules and value types |
metrics: | metrics[] (OSSIE_SQL_2026, datatype, description) | only the declared ones - see Metrics |
@terms(...) (entity, field, measure) | ai_context.synonyms | business terms |
DataTypes
Ossie has ten DataTypes: String, Integer, Decimal, Float, Boolean, Date, Time,
DateTime (no time zone), DateTimeTz (an instant), Opaque (outside the vocabulary).
| DSL | Ossie | DSL | Ossie |
|---|---|---|---|
string, text, clob | String | float, double | Float |
int, long, biginteger, quantity | Integer | boolean | Boolean |
decimal, bigdecimal, money | Decimal | date / time | Date / Time |
| an enum | String for a text code or the name, Integer for an ordinal (codes in ai_context) | datetime | DateTime |
json, blob, Object | Opaque | timestamp | DateTimeTz |
is_time is not written: the spec derives it from the datatype, and that answer is this model's.
Dialects
The spec registers twelve dialect names but defines the meaning of one, OSSIE_SQL_2026
(core-spec/expression_language.md: column references, arithmetic, comparison and logical
operators, CASE WHEN, aggregates, COALESCE, NULLIF, CAST). The others, ANSI_SQL among them,
name vendor languages a consumer may ignore. So field expressions (column names) and the expressions
built from metrics: are exported as OSSIE_SQL_2026. An @formula is split by what it holds: an
expression in that language (unit_price * quantity) is OSSIE_SQL_2026; one with a subquery
((select count(*) from ...)) cannot be said in it (SELECT, FROM, JOIN, subqueries and CTEs are not
supported in expressions), so it is exported as one ANSI_SQL entry - a reader that understands the
SQL uses it, others skip it. An @formula field also gets "formula": "database" in its
custom_extensions.
What is not exported
Measures nobody declared: inventing a revenue formula from entity fields would be exporting a guess.
An excerpt - the pet store
# Apache Ossie (incubating) semantic model - core spec 0.2.0.dev0
# Generated by ElastiCORE from the blueprint DSL - do not edit;
# regenerate with the ossie-export rule set.
version: 0.2.0.dev0
name: petstore
description: "Domain model of petstore, as its ElastiCORE DSL declares it"
ai_context:
instructions: "Each dataset is a table the generated application owns. An enumeration column holds a code; the field's ai_context lists every code and its label. Amounts (money) are decimals with their scale; a quantity is a count that is never negative. Password and write-only columns are left out on purpose. Lifecycles, access rules and derived values are in the ELASTICORE custom extensions."
datasets:
- name: PurchaseOrder
source: ps_purchase_order
primary_key: [order_id]
description: "주문"
ai_context:
synonyms: ["order", "purchase", "구매"]
fields:
- name: status
expression:
dialects:
- dialect: OSSIE_SQL_2026
expression: "status"
datatype: String
ai_context: "Codes: PLC = Placed, SHP = Shipped, CAN = Cancelled"
custom_extensions:
- vendor_name: ELASTICORE
data: "{\"lifecycle\": true, \"enumeration\": \"OrderStatus\"}"
- name: city
expression:
dialects:
- dialect: OSSIE_SQL_2026
expression: "city"
datatype: String
description: "deliveryAddress.city"
- name: total_without_vat
expression:
dialects:
- dialect: OSSIE_SQL_2026
expression: "total_without_vat"
datatype: Decimal
custom_extensions:
- vendor_name: ELASTICORE
data: "{\"compute\": \"sum(lines.subTotal)\", \"value_type\": \"money\"}"
# ...
custom_extensions:
- vendor_name: ELASTICORE
data: "{\"entity\": \"PurchaseOrder\", \"domain\": \"petstore\", \"owner\": \"customerId\", \"access\": {\"*\": \"admin\", \"cancel\": \"owner\", \"create\": \"user\", \"read\": \"owner\"}, \"states\": {\"status\": {\"initial\": \"PLACED\", \"ship\": \"PLACED -> SHIPPED\", \"cancel\": \"PLACED -> CANCELLED\", \"delete\": \"CANCELLED\"}}, \"events\": \"Placed(total), Shipped, Cancelled\", \"omitted_columns\": [\"credit_card_number\"]}"
relationships:
- name: PurchaseOrder_customer
from: PurchaseOrder
to: Customer
from_columns: [customer_id]
to_columns: [customer_id]
metrics:
- name: averageOrder
expression:
dialects:
- dialect: OSSIE_SQL_2026
expression: "(SUM(CASE WHEN PurchaseOrder.status <> 'CAN' THEN PurchaseOrder.total END)) / NULLIF((COUNT(CASE WHEN PurchaseOrder.status <> 'CAN' THEN 1 END)), 0)"
description: "주문 1건 평균 금액"
datatype: Decimal
ai_context:
synonyms: ["AOV", "객단가"]
The card number of CreditCard (@writeonly) is not a field of the dataset - it is named in
omitted_columns. The pet store's referenceColumn: short makes the join column customer_id.
How it is checked
SemanticModelTest(the pet store andelasticore-template-ddd), on every build: every exported table, column, primary key and join column exists in the real database. A semantic model that names a column the database does not have is a query that fails in somebody's BI tool.MetricsTest(the pet store): places, ships and cancels orders, then runs each exported metric's SQL on the live database (dataset names replaced by their tables) and compares it with the same numbers computed from the entities - revenue, order count, average order, shipped, cancelled, customers.- Both files were validated against Ossie's
ossie-schema.jsonwith no errors (2026-10-03, run by hand - not part of the build tests). - Unit tests in
elasticore-base:OssieExportTest,OssieMetricsTest,OssieSqlTest,OssieFormulaDialectTest.
Using the file
semantic_model.yml is meant for tools that read Ossie: a BI tool takes the datasets, joins and
metrics instead of having them modelled again; an AI agent reads ai_context (codes and their
labels, synonyms, the model's instructions) and the metric definitions, so "revenue last month" maps
to PurchaseOrder and revenue with the business's own definition, and the time axis comes from a
date field such as order_date. Lifecycles, access rules and derived values travel in the
ELASTICORE custom extensions.
History
- Before the first public release (3.0.261015-alpha1) - the export was brought to the spec: before, it wrote a
semantic_model:array withoutexpression,moneyasString, no enum or embedded columns, 2.x-style join columns and the password column. - 3.0.261022-rc1 - expressions are exported as
OSSIE_SQL_2026(before: allANSI_SQL, which a reader may skip entirely); only a subquery@formulastaysANSI_SQL;"formula": "database"added.count(Entity)counts the key column (COUNT(Customer.customer_id)) instead of a bareCOUNT(*).