v0.2.3
Latest Release Last updated: July 2026

TMSL Model Documentation

The TMSL model structure supported by dax-rs.

This document describes the subset of Tabular Model Scripting Language (TMSL) that dax-rs can read and write. The format is a JSON file that describes tables, columns, measures, and relationships. It maps directly to Power BI Desktop’s model export format.


Rust API

use dax_rs::loaders::tmsl::{load_tmsl, save_tmsl, load_tmsl_from_op, save_tmsl_to_op};
use dax_rs::catalog::Catalog;

// Load from the local filesystem
let catalog: Catalog = load_tmsl("model.json")?;

// Save back to disk (round-trips all fields)
save_tmsl("model.json", &catalog)?;

// Load / save via an opendal BlockingOperator (S3, Azure Blob, etc.)
let catalog = load_tmsl_from_op(&op, "model.json")?;
save_tmsl_to_op(&op, "model.json", &catalog)?;

load_tmsl deserialises the JSON, validates the schema, and returns a Catalog. It returns an error (not a panic) if the JSON is malformed or any validation rule is violated (see Validation).

save_tmsl serialises a Catalog back to the same JSON structure, preserving the original definition order of tables, columns, and measures. Fields that equal their default value are omitted to keep the output compact.


Top-level structure

{
  "name": "MyModel",
  "culture": "en-US",
  "collation": "Latin1_General_100_CI_AS",
  "compatibilityLevel": 1550,
  "defaultMode": "import",
  "tables": [ ... ],
  "relationships": [ ... ]
}
FieldTypeRequiredNotes
namestringnoDisplay name of the model.
culturestringnoLocale string, e.g. "en-US".
collationstringnoSQL collation name.
compatibilityLevelintegernoNumeric compatibility level, e.g. 1550.
defaultModestringnoStorage mode hint, e.g. "import".
tablesarrayyesOne entry per table (see Tables).
relationshipsarrayyesMay be empty ([]).

Tables

{
  "name": "Sales",
  "dataSource": "data/sales.parquet",
  "isHidden": false,
  "dataCategory": "Time",
  "description": "Transactional sales data",
  "columns": [ ... ],
  "measures": [ ... ]
}
FieldTypeRequiredDefaultNotes
namestringyesUnique table name within the model.
dataSourcestringyesPath to the backing Parquet file.
columnsarrayno[]See Columns.
measuresarrayno[]See Measures.
isHiddenbooleannofalseHidden tables are excluded from client tool auto-discovery.
dataCategorystringnoabsentSemantic category. "Time" is the only named variant; any other string is stored verbatim.
descriptionstringnoabsentFree-text description shown in client tools.

Columns

{
  "name": "OrderDate",
  "dataType": "dateTime",
  "summarizeBy": "none",
  "isHidden": false,
  "isKey": false,
  "isNullable": true,
  "isUnique": false,
  "formatString": "Short Date",
  "displayFolder": "Dates",
  "dataCategory": "WebUrl",
  "sortByColumn": "MonthNumber",
  "description": "Date the order was placed"
}
FieldTypeRequiredDefaultNotes
namestringyesColumn name, unique within the table.
dataTypestringyesSee Data types.
summarizeBystringno"none"See SummarizeBy.
isHiddenbooleannofalseHides the column from client tools.
isKeybooleannofalseMarks the column as the table’s primary key.
isNullablebooleannotrueWhen false, the column must not contain null values.
isUniquebooleannofalseAsserts that all values in the column are distinct.
formatStringstringnoabsentDisplay format hint, e.g. "#,##0.00", "Short Date".
displayFolderstringnoabsentFolder path shown in client tools, e.g. "Finance\\Revenue".
dataCategorystringnoabsentSemantic hint for client tools. See Column data categories.
sortByColumnstringnoabsentName of another column in the same table to sort by. The target must exist.
descriptionstringnoabsentFree-text description.

Data types

JSON valuePolars typeNotes
"string"String
"int64"Int64
"double"Float64
"decimal"Float64Stored as Float64; no fixed-precision arithmetic.
"boolean"Boolean
"dateTime"Datetime(ms)UTC milliseconds since epoch.
"binary"Binary
"automatic"Rejected at load time. Column type must be explicit.
"unknown"Rejected at load time.
"variant"Rejected at load time.

Deserialisation is case-insensitive on the way in; serialisation always emits the lowercase canonical form shown above.

SummarizeBy

Controls the default aggregation Power BI applies when the column is dropped onto a visual.

JSON valueMeaning
"none"No default aggregation (default)
"sum"Sum
"min"Minimum
"max"Maximum
"count"Count of non-blank values
"average"Average
"distinctCount"Count of distinct values

Column data categories

The dataCategory string is a hint to client tools for semantic formatting or map visuals. Recognised values:

Address, City, Continent, Country, County, Latitude, Longitude, Place, PostalCode, StateOrProvince, WebUrl, ImageUrl, Barcode, PhoneNumber, Organization, FaceUri

Any other string is accepted and stored verbatim as Other(value).


Measures

{
  "name": "Total Sales",
  "expression": "SUM(Sales[Amount])",
  "isHidden": false,
  "formatString": "#,##0.00",
  "displayFolder": "Revenue",
  "description": "Sum of all sales amounts"
}
FieldTypeRequiredDefaultNotes
namestringyesMeasure name, unique across the whole model (not just the table).
expressionstringyesDAX expression body. May reference other measures and columns.
isHiddenbooleannofalseHides the measure from client tools.
formatStringstringnoabsentDisplay format, e.g. "#,##0.00", "0%".
displayFolderstringnoabsentFolder path in client tools, e.g. "KPIs\\Revenue".
descriptionstringnoabsentFree-text description.

Measures belong to a table but are globally namespaced: the first occurrence of a given measure name wins and subsequent duplicates are silently ignored.


Relationships

{
  "name": "Sales_Product",
  "fromTable": "Sales",
  "fromColumn": "ProductSK",
  "toTable": "Product",
  "toColumn": "ProductSK",
  "crossFilteringBehavior": "single",
  "isActive": true
}
FieldTypeRequiredNotes
namestringyesUnique relationship name within the model.
fromTablestringyesThe “many” side (fact / detail table). Holds the foreign key.
fromColumnstringyesJoin column on fromTable. Must be declared in columns.
toTablestringyesThe “one” side (dimension / lookup table). Holds the primary key.
toColumnstringyesJoin column on toTable. Must be declared in columns.
crossFilteringBehaviorstringyes"single" — filter flows from toTable (dim) to fromTable (fact) only. "both" — bidirectional.
isActivebooleanyesOnly active relationships participate in automatic filter propagation.

Both join columns must have the same dataType; a type mismatch is rejected at load time.


Validation

Catalog::from_model validates the model before returning. All errors name the offending object so the message can be surfaced directly to users.

RuleError message shape
Column has dataType of "automatic", "unknown", or "variant"Column '<name>' has unsupported type …
Relationship references a column not declared in columns (from side)Relationship '<name>': from-column '<table>.<col>' not declared in model
Relationship references a column not declared in columns (to side)Relationship '<name>': to-column '<table>.<col>' not declared in model
Join columns have different dataType valuesRelationship '<name>': join column type mismatch — '<t1>.<c1>' is … but '<t2>.<c2>' is …
sortByColumn references a column that does not exist in the same tableColumn '<table>.<col>': sortByColumn '<target>' does not exist in table '<table>'

Examples

Minimal single-table model

{
  "name": "SimpleModel",
  "tables": [
    {
      "name": "Sales",
      "dataSource": "data/sales.parquet",
      "columns": [
        { "name": "OrderDate", "dataType": "dateTime" },
        { "name": "Amount",    "dataType": "double",  "summarizeBy": "sum" }
      ],
      "measures": [
        { "name": "Total Sales", "expression": "SUM(Sales[Amount])" }
      ]
    }
  ],
  "relationships": []
}

Star schema (fact + dimension with relationship)

{
  "name": "DemoModel",
  "tables": [
    {
      "name": "Sales",
      "dataSource": "data/sales.parquet",
      "columns": [
        { "name": "ProductSK", "dataType": "Int64" },
        { "name": "Amount",    "dataType": "double", "summarizeBy": "sum" },
        { "name": "Quantity",  "dataType": "double", "summarizeBy": "sum" }
      ],
      "measures": [
        { "name": "TotalAmount",   "expression": "SUM(Sales[Amount])" },
        { "name": "AmountAbove30", "expression": "CALCULATE(SUM(Sales[Amount]), Sales[Amount] > 30)" },
        { "name": "DoubleTotal",   "expression": "[TotalAmount] * 2" }
      ]
    },
    {
      "name": "Product",
      "dataSource": "data/product.parquet",
      "columns": [
        { "name": "ProductSK",   "dataType": "Int64" },
        { "name": "ProductType", "dataType": "string" },
        { "name": "Color",       "dataType": "string" }
      ],
      "measures": []
    }
  ],
  "relationships": [
    {
      "name": "Sales_Product",
      "fromTable": "Product",
      "fromColumn": "ProductSK",
      "toTable": "Sales",
      "toColumn": "ProductSK",
      "crossFilteringBehavior": "single",
      "isActive": true
    }
  ]
}

The relationship direction matters: filters flow from toTable (the dimension / “one” side) to fromTable (the fact / “many” side). In this example Product[Color] = "Red" automatically restricts which Sales rows are visible.

Snowflake schema (two-hop relationships)

{
  "name": "SnowflakeModel",
  "tables": [
    {
      "name": "Sales",
      "dataSource": "data/sales.parquet",
      "columns": [
        { "name": "ProductSK", "dataType": "Int64" },
        { "name": "Amount",    "dataType": "double" }
      ],
      "measures": []
    },
    {
      "name": "Product",
      "dataSource": "data/product.parquet",
      "columns": [
        { "name": "ProductSK",  "dataType": "Int64" },
        { "name": "CategorySK", "dataType": "Int64" }
      ],
      "measures": []
    },
    {
      "name": "Category",
      "dataSource": "data/category.parquet",
      "columns": [
        { "name": "CategorySK",   "dataType": "Int64" },
        { "name": "CategoryName", "dataType": "string" }
      ],
      "measures": []
    }
  ],
  "relationships": [
    {
      "name": "Sales_Product",
      "fromTable": "Product",
      "fromColumn": "ProductSK",
      "toTable": "Sales",
      "toColumn": "ProductSK",
      "crossFilteringBehavior": "single",
      "isActive": true
    },
    {
      "name": "Product_Category",
      "fromTable": "Category",
      "fromColumn": "CategorySK",
      "toTable": "Product",
      "toColumn": "CategorySK",
      "crossFilteringBehavior": "single",
      "isActive": true
    }
  ]
}

Filter propagation chains: a Category[CategoryName] filter reaches Sales automatically via the two-hop path Category → Product → Sales.

Column metadata (sorting, folders, categories)

{
  "name": "CalendarModel",
  "tables": [
    {
      "name": "Date",
      "dataSource": "data/date.parquet",
      "dataCategory": "Time",
      "columns": [
        { "name": "DateKey",     "dataType": "Int64",    "isKey": true,   "isUnique": true, "isNullable": false },
        { "name": "MonthName",   "dataType": "string",   "sortByColumn": "MonthNumber", "displayFolder": "Month" },
        { "name": "MonthNumber", "dataType": "Int64",    "isHidden": true, "displayFolder": "Month" },
        { "name": "CalendarURL", "dataType": "string",   "dataCategory": "WebUrl" }
      ],
      "measures": []
    }
  ],
  "relationships": []
}