Validating FIX messages

parse answers “what does this message say?”; validate answers “is it allowed to say that?”. It checks presence of required fields, enum membership, datatype formats and conditional-required rules against the dictionary, and returns a FixIssue[] — empty when the message obeys the rules, and never an exception when it does not.

The FixIssue shape

Every finding — from parse and validate alike — is the same structure: a stable code, a severity, a human-readable message, and, where they apply, a path into the parsed structure plus FIX-ready reference fields (refTagID, refSeqNum, refMsgType, sessionRejectReason) for building a Reject without re-deriving anything.

validate-issues.mtsts
import { createFixEngine } from '@boarteam/fix';import { dictionary } from '@boarteam/fix-dict-fix44';const fix = createFixEngine(dictionary);// Framing is perfect — BodyLength and CheckSum both check out. But OrdStatus// (39) holds "Z", which FIX 4.4 does not define, and AvgPx (6), required in// every ExecutionReport, is missing.const raw =  '8=FIX.4.4|9=148|35=8|49=BROKER|56=BUYSIDE|34=8|52=20240101-12:00:02.000|' +  '37=ORD-2|17=EXEC-2|150=F|39=Z|55=EUR/USD|54=1|38=500000|32=500000|31=1.0919|' +  '151=0|14=500000|10=105|';const { message, issues } = fix.parse(raw, { soh: '|' });console.log(issues.length); // → 0// validate() checks required fields, enum membership, datatype formats and// conditional-required rules against the dictionary — and returns FixIssue[]:const problems = fix.validate(message);for (const problem of problems) {  console.log(problem.severity, problem.code, problem.path);}// → error validate/value-not-in-enum OrdStatus// → error validate/required-field-missing AvgPx// A FixIssue in full. `code` is stable and machine-readable — match on it.// `message` is for humans and NOT covered by SemVer — never match on it.console.log(JSON.stringify(problems[0], null, 2));/* →{  "code": "validate/value-not-in-enum",  "severity": "error",  "message": "Field OrdStatus (39) value \"Z\" is not an allowed value.",  "refTagID": 39,  "path": "OrdStatus",  "sessionRejectReason": 5}*/

Issue codes are the contract; messages are not

The set and meaning of the code strings — parse/checksum-mismatch, validate/value-not-in-enum and friends — are part of the package’s SemVer contract, alongside the output shapes and the accepted inputs. Pin a version and the codes you assert on will not change underneath you; while the package is on 0.x, anything that would break them ships as a minor bump. The human message text carries no such promise: log it, display it, but never match on it.

The codes you are most likely to meet, and what usually causes them, are catalogued in decode diagnostics.

Severities

Issues carry one of three severities. error means the message is wrong in a way that matters — a failed checksum, a value outside its enum, a missing required field. warning means suspect but usable. info is a note. Severity is the only verdict there is: framed: true is never “valid”, and an empty issue list at every severity is the strongest statement the engine makes.

Validation as a CI assertion

Because findings are plain data from a pure function, FIX correctness slots into an ordinary test runner — no session to stand up, no counterparty to mock. Keep golden fixtures in the repo and assert they stay clean; assert that known-bad fixtures keep producing exactly the codes they should.

validate-ci-assert.mtsts
import assert from 'node:assert/strict';import { createFixEngine } from '@boarteam/fix';import { dictionary } from '@boarteam/fix-dict-fix44';const fix = createFixEngine(dictionary);// Because parse and validate return plain data, a FIX correctness check is an// ordinary test assertion — no session, no socket, no mock counterparty.// Drop this in a test file and CI fails the moment a fixture goes bad.const goldenOrder =  '8=FIX.4.4|9=137|35=D|49=BUYSIDE|56=SELLSIDE|34=42|52=20240101-12:00:00.000|' +  '11=ORDER-1|55=EUR/USD|54=1|60=20240101-12:00:00.000|38=1000000|40=2|44=1.0921|10=161|';const { message, issues } = fix.parse(goldenOrder, { soh: '|' });assert.deepEqual(issues, []);assert.deepEqual(fix.validate(message), []);assert.equal(message.name, 'NewOrderSingle');assert.equal(message.fields[44].value, 1.0921);console.log('golden order still parses and validates clean'); // → golden order still parses and validates clean