Your mapping spec is the implementation.

Your analysts already keep a spreadsheet that says what every field in your regulatory and surveillance feeds should contain. Today someone else turns it into code. expressionmap runs the spreadsheet itself: point it at your trade data, and it writes the feed with every field checked and every value explained.

Runs on your batch schedule Any table you provide JSON and CSV out
One row in the mapping workbook
Target fieldExpressionValidation
Economics.Amount quantity * (buySell == 'Sell' ? -1 : 1) Amount == 0 ? 'zero quantity' : ''
What the consumer receives"Economics": {
  "Amount": -1000.0,
  "Price": "81.82"
}
Why each field holds that value"Economics": {
  "Amount": "quantity=1000.0, buySell=Sell",
  "Price": "tradePrice=81.82"
}

The problem

The mapping already exists. It just doesn't run.

In most trading firms the business owns a mapping spreadsheet and engineering owns a transformation layer that is supposed to match it. The two drift. Every fix waits for a release, and nobody outside engineering can see what actually runs.

Today

Trade data→Mapping spreadsheet→Transformation code→Feed
  • Every field change is a ticket. A new venue code or a changed rounding rule is a build and a release. The analyst who knows the answer waits for someone else to type it.
  • Nobody outside engineering can read the logic. The person who knows what a field should contain cannot see how it is computed.
  • One bad field fails the record. The error names a stack frame, not the value that was wrong.
  • "Why does this field say that?" is a multi-layer trace. Audit asks; engineering walks back through staging layers that flattened the data on the way.
  • Cost scales with the code, not the mapping. Support and maintenance grow with every line of transformation code, long after the mapping itself stopped changing.

With expressionmap

Trade data→Mapping workbook→Feed
  • A field change is a workbook edit and a review. There is no generated code and nothing to compile, so nothing to release.
  • The file the business reviews is the file that runs. One expression per field, in a syntax an analyst can read.
  • The failing field is named, in words. The rest of the record still maps. Nothing fails silently.
  • Every field carries its own explanation. The path that was read and the value it held, written beside the feed on every run.
  • Cost scales with the mapping. The footprint of a feed is its workbook. There is no transformation code to maintain.

How it works

From your table to a validated feed in four steps. None of them is a release.

Step 1 · engineering, once

Declare the source

Name the table, the row filter and the workbook in one schema file. The source tree and every field type are read from your data, nested as it already is.

Step 2 · generated

Generate the workbook

The first version of the mapping is produced from the schema, not typed. The target tree is written by indentation: nesting in the sheet is nesting in the output.

Step 3 · analysts

Fill in one expression per field

A plain path for the common case, a real expression when needed. Format and validation rules sit beside the value in the same row. Lookups and reference calls are rows too.

The analyst who knows the field writes the row. A developer reviews. That is the reverse of today.

Step 4 · the engine, every batch

Validate, then transform

Every expression is checked against the declared types before a single row is read. Then the batch runs and writes the feed, with its evidence beside it.

The workbook

One sheet carries the whole mapping. Here is what each column does.

The source tree, the target tree and one expression per field, in a file an analyst can read and the engine can run. Format and validation sit beside the value they govern, so presentation and rules never move into code.

Source section · the shape of your table, declared once
S1S2S3Source type
trade
tradeIdString
buySellString
quantityDouble
tradePriceDouble
instrument
exchangeString
lastTradeableDateString

Declared once, read everywhere

The source columns hold the tree your table already has, nested as it is, with the type of each leaf. Nothing is flattened on the way in. Every expression on the target side is checked against this declaration before a row is read.

Generated, not typed

The first version of this section is produced from your schema. Analysts never transcribe a data model.

Target section · the feed, one row per field
T1T2T3T4MandatoryTarget expressionFormatValidation
Execution
TradeIDtruetrade.tradeId
ExtractDatetrueLocalDate.now()yyyy-MM-dd
Economics
Pricetruetrade.tradePrice#.####
Amounttruequantity * (buySell == 'Sell' ? -1 : 1)Amount == 0 ? 'zero quantity' : ''
Identifier
ExchangetruegetExchange(instrument.exchange) ?: instrument.exchangeExchange == 'NYM' ? 'should be mapped to XNYM' : ''

Target columns

The feed's tree, written by indentation. Nesting in the sheet is nesting in the output, and nobody codes the structure.

Mandatory

true or false per field. A missing value is reported on that field, in words. Nothing is thrown.

Target expression

How the value is computed. A plain path for the common case, a real expression when the field needs one.

Format

A pattern for numbers and dates. Presentation stays separate from computation.

Validation

A rule that may read other fields. It runs as the record is produced and says why the field failed.

Indentation is nesting

The rows above, as the engine writes them

Each target row becomes one field. A row with no expression is a node, and the rows indented beneath it are its children. The feed on the right was written from the sheet on the left with no code in between.

Flat output works the same way: for CSV, each leaf row becomes a column.

valid.json · one record, abridged"Execution": {
  "TradeID": "T-20931",
  "ExtractDate": "2026-09-16",
  "Economics": {
    "Price": "81.82",
    "Amount": -1000.0,
    "Identifier": {
      "Exchange": "XNYM"
    }
  }
}
MappingThe source and target sections above. One per feed.
VariablesValues computed once per record and reused, such as the client, the counterparty or the instrument record.
FunctionsReference calls the variables use, declared with a signature so their arguments are checked too.
LookupsCode tables as plain sheets. A missing venue code is fixed by adding a row, not by a release.

For the business

The people who know the data own the mapping.

The workbook your analysts review is the workbook the engine runs. A missing lookup value is fixed by adding a row. A changed rule is an edit and a review. Onboarding a new product is measured in analyst days, not sprints.

  • One expression per target field, readable by the person who owns it
  • Validation rules live beside the value they govern
  • Reference data and lookups are sheets, not services to deploy

Expressions from a real workbook

Exchange
getExchange(instrument.exchange) ?: instrument.exchange
Counterparty
counterparty?.legalEntityId ?: counterparty?.partyId
Year code
'' + instrument.lastTradeableDate.substring(0, 4)
Extract date
LocalDate.now()

For operations

Mistakes surface before the data moves, and name the field when they don't.

When the workbook loads, every expression is checked against the declared source types. A typo is refused with a row number before a single row is read. At run time, one bad field does not sink the record: the rest maps, and the failure names the field and the reason in words an analyst can act on.

  • Every expression type-checked at load time
  • Validation runs as the record is produced, not after the fact
  • Every record lands in exactly one place: valid, invalid or error

One run, three files

valid.jsonMapped and passed every rule
invalid.jsonWhich field failed which rule
error.jsonWhich field threw, and why
Refused when the workbook loadsline=7 · The property [trade.tradeId2] is undeclared
Reported per field, per record"Exchange": "should be mapped to XNYM"

For audit and support

Every document carries its own explanation.

Beside every output file, the engine writes a source file in the same shape. For each field it records the path that was read and the value it held at the moment the field was produced. The feed says what was sent. The source file says why, for every field, for every record, on every run. An analyst lines them up side by side.

  • Same tree as the output, field for field
  • Every input named, including lookup and reference results
  • Produced on every run, not reconstructed on request

Today: one question, four hops

  1. Feed value
  2. Final staging layer
  3. Cleaned and joined layer
  4. Raw copy
  5. Source system

With expressionmap: one hop

  1. Feed value
  2. Source path and value, on the same field

For engineering

One processor for every feed. Transformation code stops multiplying.

The engine is a library that follows the workbook. It is the same binary for every feed, every table and every output shape, so the code footprint for a new feed is the workbook: in most cases zero lines of custom code.

A closed, typed expression language

Java-like expressions over ten source types, with safe navigation, elvis and ternaries. No statements, no assignment, no loops. Only a short list of classes and methods can be named; anything else is refused by the validator. Money math is exact by default.

The full expression reference

Workbooks are test fixtures

A workbook, a sample record and an expected output run as a unit test on every build. A bad workbook never reaches production: it fails to load, with the row number, before any data flows.

One feed, four runs, step by step

Fits the batch you already run

Four commands: schema, mapping, validate, transform. Source is any table you expose. Reference data arrives over REST. Output is nested JSON or flat CSV from the same workbook, written where your consumer already reads.

One schema file names every feed

The table, the row filter and the workbook, one entry per product. Adding a table is one more entry and a generated workbook.

schema:
  tables:
    commodityFuture:
      table:   "listed_derivative_trade"
      filter:  "product.productType = 'COMMODITY_FUTURE'"
      mapping: "commodityFuture.xlsx"
    # one entry per product or source system

Four commands, no code between them

Each command reads the file the previous one wrote. The analyst's work sits between the second and the third.

$ run.sh schema
  read the schema file, write a type schema per table
$ run.sh mapping
  generate the first workbook from the schema
  (analysts then fill in the Target expression column)
$ run.sh validate
  check every expression against the declared types
$ run.sh transform
  map the rows, write the feed and its evidence

Same row, nested or flat

The workbook does not know which shape it feeds. The same leaf row writes a node in JSON and a column in CSV.

T2T3Target expressionFormat
EconomicsPricetrade.tradePrice#.####
"Economics": {
  "Price": "81.82"
}
TradeID,Economics.Price
T-20931,81.82

Guard rails, in the order they fire

Everything the validator can know from the declared types is refused before any data. Whatever it could not know statically is caught per field at evaluation, with the same wording.

Validator · before any row is read Unknown variable: tradeId2 No such property 'venue' on instrument No such method 'round' with 0 argument(s) on Double Unsupported method: collect Unsupported static method: Integer.parseInt Division by literal zero
Evaluator · per field, per record Cannot get property 'contractSize' on null Cannot compare Integer with String Unknown constant: MAX_SIZE Error invoking getExchange: ...
0lines of custom transformation code for a typical feed
1file to review: the workbook is the implementation
3outcomes per record, with the field named
100%of fields explained on every run

The pilot

Five steps, one cycle, and a comparison you can read.

  1. 1Declare the source

    Engineering names the table, the row filter and the workbook in the schema file.

  2. 2Generate the workbook

    The first version is produced from your schema, source tree and types included.

  3. 3Analysts fill it in

    One expression per target field, with format and validation beside it. Lookups become sheets.

  4. 4Run beside your feed

    The engine runs on your batch schedule for one cycle, writing its own output next to the current one.

  5. 5Compare field by field

    Two feeds, lined up. Every difference names its field and carries its explanation.

A pilot feed, run beside your current output.

Pick one feed. We map it from the table you already expose, run it on your batch schedule for one cycle, and compare the two outputs field by field. You keep the workbook either way.

Request a pilot See a feed run end to end hello@expressionmap.com