Transform Reference

ODIN Transform 1.0, Declarative Data Transformation

Overview

ODIN Transform extends the ODIN notation to define data transformations between ODIN documents and external formats. A transform file is itself a valid ODIN document that describes how to convert data, the notation transforms itself.

Transform definitions are declarative mappings: source paths map to target fields through copy operations, transformations, and structural directives. The same transform engine handles fixed-width records, XML hierarchies, delimited files, and JSON output.

Design Principles

  1. Declarative, Mappings describe what, not how
  2. Unidirectional, One transform file, one direction
  3. Self-describing, Operations are explicit in the syntax
  4. Composable, Import and reuse common mappings
  5. Format-agnostic, Same concepts across output formats
  6. Traceable, Source and target schemas documented
  7. Deterministic, Same input produces same output

ODIN as a Canonical Data Model

ODIN serves as the Canonical Data Model (CDM) for all transformations. Regardless of source and target formats, data flows through ODIN as the intermediate representation.

Source
Odin
JSON
XML
CSV
Fixed-Width
ODIN CDM
Target
Odin
JSON
XML
CSV
Fixed-Width

Why ODIN as CDM?

  • Type Preservation, ODIN's self-describing types (#, ##, #$, ?, @, ^, ~) preserve semantic meaning
  • Modifier Flow, Field modifiers (! critical, * confidential, - deprecated) travel with data
  • Canonical Form, Enables hashing, signatures, and deduplication
  • Schema Validation, ODIN Schema validates the canonical form

A transform declared as json->json actually executes as json->odin->json.

Direction Header

Every transform declares its direction in the {$} header:

Transform header
{$}
odin = 1.0.0
transform = 1.0.0
id = org.example.transform.json.policy
name = Policy to JSON
direction = odin->json

Document Structure

Transform documents use the .transform.odin extension and include several standard headers. The {$source} section describes the incoming data format, while {$target} controls how the output is rendered.

Source and target declarations
{$source}
schema = org.odin.auto.policy/1.0.0
format = "json"
discriminator = @.recordType      ; For multi-record-type inputs

{$target}
format = json
encoding = utf-8
indent = ##2

The discriminator enables multi-record processing, when input contains mixed record types, the engine routes each record to the matching segment based on the discriminator field value.

Confidential Field Enforcement

The :confidential modifier flags a field as containing sensitive information, Social Security numbers, API keys, medical records, financial account numbers, or any data subject to privacy regulations. On its own, the modifier does not alter values. It travels with the data as metadata, signaling to downstream systems, loggers, and audit trails that the field requires special handling.

Flagging fields as confidential
{Customer}
SSN = @.ssn :confidential              ; Flagged, value passes through unchanged
API_Key = @.apiKey :confidential       ; Downstream systems know to mask in logs
Name = @.name                          ; Not confidential, no special handling

When you need the transform itself to protect confidential values before they leave, set enforceConfidential in the header. This activates enforcement -- every field marked :confidential will have its value redacted or masked in the output.

Activating enforcement
{$}
enforceConfidential = "redact"   ; or "mask"

{Customer}
SSN = @.ssn :confidential          ; Value becomes null (redact) or "***-**-****" (mask)
Name = @.name                      ; Not confidential, passes through normally
enforceConfidentialBehavior
(not set)No enforcement, confidential values pass through with the * modifier as metadata
"redact"All confidential values become ~ (null)
"mask"Confidential strings become asterisks (same length), numbers and booleans become ~

This two-layer design lets you flag fields once and control enforcement per transform. A transform feeding an internal data warehouse might leave values intact (just flagged), while a transform generating a partner export redacts them.

Constants & Accumulators

Constants define reusable values that stay fixed throughout the transform. Use them for default values, version strings, or any value referenced in multiple places -- change it once in the header instead of hunting through mappings.

Constants
{$const}
DEFAULT_STATE = "TX"
POLICY_VERSION = "2024.1"
MAX_VEHICLES = ##10

; Reference with @$const.NAME
state = @policy.state :default @$const.DEFAULT_STATE

Accumulators are mutable counters that update as the transform processes records. They're useful for running totals, record counts, and summary fields that appear in headers or trailers.

Accumulators
{$accumulator}
record_count = ##0
premium_total = #$0.00

; Update with %accumulate
_ = %accumulate record_count ##1
_ = %accumulate premium_total @coverage.premium

; Read with @$accumulator.name
total_records = @$accumulator.record_count

Lookup Tables

Lookup tables embed reference data directly in the transform, replacing what would otherwise require database joins or external API calls. Define columns in the header, then use %lookup to search by any column and return any other column.

Lookup tables
{$table.STATUS[name, code]}
active, "A"
pending, "P"
cancelled, "C"

; Lookup by name, return code
status_code = %lookup STATUS.code @.statusName

; Reverse lookup (same syntax)
status_name = %lookup STATUS.name @.statusCode
Multi-column tables
{$table.TERRITORY[state, zip_prefix, territory, region]}
TX, 787, "AUSTIN", "CENTRAL"
TX, 750, "DALLAS", "NORTH"

; Match on multiple columns
territory = %lookup TERRITORY.territory @.state @.zipPrefix

Symbol Reference

SymbolNameMeaning
@CopyDirect copy from source path
@.CurrentCurrent loop item
@$accumulatorAccumulatorReference accumulator value
@$constConstantReference constant value
%TransformApply built-in verb
%&CustomApply custom namespaced verb
"..."LiteralConstant string value
##NIntegerConstant integer value
#NNumberConstant numeric value
#$NCurrencyConstant currency value
:ModifierModifier follows

Mapping Syntax

Every line in a transform body is a mapping: target = expression. The left side names the output field, the right side defines where the value comes from -- a source path, a literal, or a verb transformation.

Direct Copy

The @ symbol copies a value from the source data to the target field. Use dot notation to navigate nested objects.

Direct copy
policy_number = @policy.number
insured_name = @insured.name.last

Constant Values

Assign literal values directly. Strings use quotes, integers use ##, numbers use #, and currency uses #$.

Constant values
record_type = "01"
count = ##0
amount = #$100.00

Transformations

The % symbol invokes a built-in verb that transforms the value. Verbs take source paths, literals, or other verb results as arguments.

Transformation expressions
effective = %formatDate @policy.effective "YYYYMMDD"
name = %concat @name.last ", " @name.first
code = %lookup STATUS @policy.status

Nested verbs evaluate inner-to-outer:

Nested verbs
display_name = %upper %concat @first " " @last

Modifiers follow the value expression:

Mapping modifiers
policy_number = @policy.number :pos 0 :len 15 :rightPad " "
state = @policy.state :default "TX"
amount = @premium :required

Transformation Verbs

70+ built-in verbs organized into nine groups. Each page includes syntax tables, examples, and "Try It" buttons that open examples in the Transform Lab.

Custom Verbs

Custom verbs use the %& prefix with a reverse-domain namespaced identifier: %&com.acme.sha256 @document.content. Namespaces under org.odin.* are reserved for official extensions.

Field Modifiers

Modifiers follow a mapping expression and control how the value is positioned, formatted, validated, or conditionally included. They use the : prefix and can be chained: @value :pos 0 :len 10 :rightPad " " :required.

Positioning Modifiers (Fixed-Width)

ModifierSyntaxDescription
:pos:pos NStart position (0-based)
:len:len NField length
:leftPad:leftPad "c"Left-pad character
:rightPad:rightPad "c"Right-pad character
:truncate:truncateTruncate to length
Positioning examples
policy_number = @policy.number :pos 0 :len 15 :rightPad " "
year = @vehicle.year :pos 15 :len 4 :leftPad "0"

XML Modifiers

ModifierSyntaxDescription
:element:elementEmit as XML element
:attr:attrEmit as attribute
:ns:ns prefixNamespace prefix
:cdata:cdataWrap in CDATA
:omitEmpty:omitEmptyOmit if empty
XML modifier examples
{Policy}
id = @policy.id :attr
Number = @policy.number
Description = @policy.description :cdata
Notes = @policy.notes :omitEmpty

JSON Modifiers

ModifierSyntaxDescription
:type:type string|number|booleanForce JSON type
:omitNull:omitNullOmit if null
:omitEmpty:omitEmptyOmit if empty string/array
:raw:rawEmit raw JSON (no escaping)
:array:arrayForce as single-element array
:object:object {key=@path}Inline object construction
JSON modifier examples
year = @vehicle.year :type number           ; "2024" -> 2024
active = @is_active :type boolean           ; "true" -> true
code = @zip :type string                    ; 78701 -> "78701"

contact = :object {name = @insured.name, phone = @insured.phone}

Conditional Modifiers

ModifierSyntaxDescription
:if:if pathInclude if truthy
:if:if path = valueInclude if equals
:unless:unless pathInclude if falsy
:default:default valueFallback value
:required:requiredError if missing
Conditional modifier examples
state = @policy.state :default "TX"
premium = @premium :required
discount = @discount :if hasDiscount
legacy_field = @old_field :unless isNewFormat

Validation Modifiers

ModifierSyntaxDescription
:validate:validate "regex"Regex validation
:enum:enum v1,v2,v3Enum validation
:range:range min..maxRange validation
:confidential:confidentialMark for confidential enforcement
Validation modifier examples
email = @contact.email :validate "^[^@]+@[^@]+$"
status = @status :enum "A,P,C"
year = @year :range 1900..2100

Segments

Segments organize transform output into logical sections, header records, detail lines, trailer summaries, or nested object groups. Each segment can have its own loop, condition, and context, making it straightforward to produce structured output from flat or hierarchical input.

Segment examples
{segment.HDR}
; Header record fields

{segment.DET}
; Detail record fields

Array Segments with Loop

Array segments
{segment.VEH[]}
:loop vehicles
vin = @.vin
year = @.year

Nested Loops

Nested loops
{segment.COV[]}
:loop vehicles :as veh
:loop .coverages :as cov
vehicle_vin = @veh.vin
coverage_code = @cov.code

Segment Directives

DirectiveSyntaxDescription
_pass_pass = ##NExecute in pass N
:type:type "value"Handle records where discriminator equals value
:loop:loop pathIterate array
:loop:loop path :as aliasIterate with alias
:counter:counter nameLoop counter
:from:from pathSet context
:if:if exprConditional segment
:literal:literalLiteral block mode

Format Configuration

The {$target} header controls how the transform engine renders output. Each format has its own set of options that affect layout, encoding, and edge-case handling.

JSON is the most common target format. These options control how the output handles whitespace, null values, and empty collections.

JSON configuration
{$target}
format = json
indent = ##2           ; Pretty-print with 2-space indentation
nulls = omit           ; Drop fields with null values from output
emptyArrays = include  ; Keep empty arrays ([] ) in output
SettingTypeDefaultDescription
indentInteger0Number of spaces for indentation. 0 produces compact single-line JSON, 2 or 4 produces human-readable output.
nullsomit | includeincludeWhen set to omit, fields whose value resolved to null are dropped entirely from the JSON output rather than appearing as "field": null.
emptyArraysomit | includeincludeWhen set to omit, array fields that contain no elements are dropped from the output rather than appearing as "field": [].
encodingStringutf-8Character encoding for the output.

XML output supports declarations, namespaces, and element-level control. Use field-level :attr and :cdata modifiers (see Field Modifiers above) to control how individual values are rendered.

XML configuration
{$target}
format = xml
declaration = true     ; Include <?xml version="1.0"?> at top
indent = ##2           ; Pretty-print with 2-space indentation
omitEmpty = true       ; Skip elements with no value

{$target.namespace}
ins = http://www.example.org/insurance/xml/
SettingTypeDefaultDescription
declarationBooleantrueInclude the <?xml version="1.0" encoding="..."?> declaration at the top of the output.
indentInteger0Number of spaces for indentation. 0 produces compact XML.
omitEmptyBooleanfalseWhen true, elements with empty or null values are omitted from the output entirely.
rootElementString(auto)Name of the root XML element. If not specified, the engine generates one from the segment structure.
encodingStringutf-8Character encoding declared in the XML header.

Namespaces are declared in a separate {$target.namespace} section. Each entry maps a prefix to a URI. Fields in the transform can then use :ns prefix to assign elements to a namespace.

CSV output writes one row per record. The field order in the transform determines column order. Use header to control whether a header row with field names is written first.

CSV configuration
{$target}
format = csv
delimiter = ","        ; Field separator (comma, pipe, tab, etc.)
quote = "\""          ; Character used to wrap fields containing delimiters
header = true          ; Write a header row with column names
lineEnding = \n       ; Line ending (\n or \r\n)
SettingTypeDefaultDescription
delimiterString,Character separating fields. Use | for pipe-delimited, \t for tab-delimited.
quoteString"Character wrapping fields that contain the delimiter, newlines, or the quote character itself.
headerBooleantrueWhen true, the first output row contains column names derived from the target field names.
lineEndingString\nLine ending sequence. Use \r\n for Windows-style line endings.

Fixed-width output produces records where each field occupies an exact number of characters. This format is common in insurance, banking, and government data exchange (ACORD, NACHA, EDI). Use :pos and :len modifiers on individual fields to control positioning.

Fixed-width configuration
{$target}
format = fixed-width
lineWidth = ##200      ; Total characters per line
padChar = " "          ; Fill unused positions with spaces
truncate = true        ; Truncate values that exceed :len
lineEnding = \r\n    ; CRLF line endings (common in mainframe formats)
SettingTypeDefaultDescription
lineWidthInteger(auto)Total characters per output line. When set, every line is padded to exactly this width. When omitted, line width is determined by the field positions.
padCharString" "Default character used to fill positions not covered by field values. Individual fields can override with :leftPad or :rightPad.
truncateBooleanfalseWhen true, values longer than the field's :len are silently truncated. When false, overflow produces a warning.
lineEndingString\nLine ending sequence. Many legacy systems expect \r\n.

Error Configuration

Error settings control how the transform engine responds to problems during execution. These apply across all formats and determine whether a bad field stops the entire transform, produces a warning, or is silently skipped.

Error handling
{$target}
onError = fail       ; fail | warn | skip
onMissing = skip     ; fail | warn | skip | default
onValidation = fail  ; fail | warn | skip
SettingValuesDefaultDescription
onErrorfail | warn | skipfailWhat happens when a verb or expression throws an error. fail aborts the transform, warn logs it and continues, skip silently omits the field.
onMissingfail | warn | skip | defaultskipWhat happens when a source path doesn't exist in the input. default uses the field's :default modifier value if one is set.
onValidationfail | warn | skipfailWhat happens when a :validate, :enum, or :range check fails.

Key Interactions

  • :required overrides onMissing, always fails if missing
  • :default "value" provides fallback when onMissing = default
  • :optional explicitly marks field as skippable

Imports & Composition

Transforms can import shared lookup tables, constants, and mapping fragments from other files. This avoids duplication when multiple transforms share common reference data or field mappings. Imported files are merged into the current transform at parse time.

Import directive
@import ./tables/common.odin
@import ./mappings/base.odin as base

Shared Lookup Tables

Shared lookup tables
;; tables/territories.odin
{$table.TERRITORY[state, zip_prefix, territory, region]}
TX, 750, "DALLAS", "NORTH"
TX, 787, "AUSTIN", "CENTRAL"
; ... thousands of entries ...

;; transforms/policy-export.odin
@import ./tables/territories.odin

{segment.VEH[]}
:loop vehicles
territory = %lookup TERRITORY.territory @.state @.zipPrefix

Shared Mappings

Shared mappings
;; mappings/address.odin
{@address}
line1 = @.line1
city = @.city
state = @.state
zip = @.zip

;; transforms/customer-export.odin
@import ./mappings/address.odin as addr

{customer.billing}
= @addr.address :from @.billing_address