Skip to content

Latest commit

 

History

60 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

@boarteam/fix

The free, zero-dependency, TypeScript-first FIX analyzer that runs in the browser. Paste a raw FIX message — get back named fields, repeating groups expanded into real nested objects, and every framing / datatype / validation problem as data. No license key. No backend. No session engine.

npm version CI docs dependencies license

📖 Documentation · 🧭 API reference · ▶️ Playground · 📚 FIX reference

Terminal recording: a raw FIX 4.4 log line is piped into @boarteam/fix and decoded in narrated stages — named, typed fields, the NoMDEntries repeating group expanded into nested Bid/Offer objects, then a clean parse + validate verdict. Next a corrupted Execution Report is piped in and still parses without throwing: the bad float, wrong checksum, and invalid OrdStatus enum all come back as structured diagnostics from parse() and validate(). The same pure engine runs unchanged in a browser tab or in Node.

A raw FIX log line is piped into examples/demo-decode.mjs and decoded in stages — raw blob → named, typed fields → the NoMDEntries repeating group expanding into nested Bid/Offer objects → a clean verdict. Then a deliberately-corrupted message is piped in: it parses anyway, every defect returned as data instead of an exception. It's a small example script calling the library (not a bundled CLI); the same pure engine runs unchanged in a browser tab — no Node-only APIs in the core. Generated from .github/demo.tape via scripts/render-demo.sh, so it stays correct by construction.

import { createFixEngine } from '@boarteam/fix';
import { dictionary } from '@boarteam/fix-dict-fix44';

const fix = createFixEngine(dictionary);

// A Market Data Snapshot (35=W). SOH shown as | for readability.
const raw =
  '8=FIX.4.4|9=130|35=W|49=SENDER|56=TARGET|34=2|52=20240101-12:00:00.000|' +
  '55=EUR/USD|268=2|269=0|270=1.0921|271=1000000|269=1|270=1.0923|271=1000000|10=248';

const { message, issues } = fix.parse(raw, { soh: '|' }); // never throws

message.msgType; // "W"
message.name; // "MarketDataSnapshotFullRefresh"
message.framed; // true
message.fields[55].value; // "EUR/USD"   (name: "Symbol")

issues; // []  — framing, checksum, and datatypes all verified clean
fix.validate(message); // []  — required fields, enums, and conditional rules all pass

The decoded message — note the 268 repeating group is an array of nested objects (not parallel arrays):

{
  "msgType": "W",
  "name": "MarketDataSnapshotFullRefresh",
  "framed": true,
  "fields": {
    "55": { "tag": 55, "name": "Symbol", "raw": "EUR/USD", "value": "EUR/USD" },
  },
  "groups": {
    "268": [
      // MDEntryType 0 = Bid; price + size come back as typed numbers, not strings
      {
        "fields": {
          "269": { "name": "MDEntryType", "value": "0" },
          "270": { "name": "MDEntryPx", "value": 1.0921 },
          "271": { "name": "MDEntrySize", "value": 1000000 },
        },
      },
      // MDEntryType 1 = Offer
      {
        "fields": {
          "269": { "name": "MDEntryType", "value": "1" },
          "270": { "name": "MDEntryPx", "value": 1.0923 },
          "271": { "name": "MDEntrySize", "value": 1000000 },
        },
      },
    ],
  },
}

Malformed input is data, never an exception. You have logs full of half-truncated, corrupted, or hand-edited messages, and you have watched free FIX parsers die on them. This one does not. Feed it 39=Z (not a valid OrdStatus), 6=not-a-number, and a wrong BodyLength / CheckSum, and parse still returns — the problems come back in issues:

// fix.parse(brokenRaw) — still no throw; message is populated, issues describe the damage:
{ "code": "parse/invalid-float", "severity": "error",
  "message": "Field AvgPx (6) value \"not-a-number\" is not a valid float.", "path": "AvgPx" }
{ "code": "parse/checksum-mismatch", "severity": "error",
  "message": "CheckSum is 000 but the computed value is 060." }
{ "code": "parse/body-length-mismatch", "severity": "warning" }

// fix.validate(message):
{ "code": "validate/value-not-in-enum", "severity": "error",
  "message": "Field OrdStatus (39) value \"Z\" is not an allowed value.", "path": "OrdStatus" }
// ...plus validate/required-field-missing for any absent required fields.

That is the whole pitch: paste a message, get the structure and every defect, in the browser, with zero dependencies. No socket, no sequence numbers, no connect(). Try it without installing anything — the playground runs this engine client-side; the guides and API reference are at boar.team/fix/docs.


Where it fits

There is a real gap in the FIX tooling landscape, and @boarteam/fix aims squarely at it: a library you can drop into a log viewer, a web dashboard, a test harness, or a CI assertion to decode, validate, and re-encode FIX — with no commercial gate and no server-side session machinery.

Point-in-time snapshot, June 2026. The alternatives are capable tools — this table is about category fit, not quality. The honest differentiators here are browser support, zero dependencies, the analyzer (not session-engine) focus, and the never-throws contract.

Library License Price Browser Runtime deps Parse Validate Encode Never-throws FIX versions Stars npm/mo.
@boarteam/fix (TS) Apache-2.0 Free Browser + Node 0 ✅ ✅ ✅ ✅ 5.0SP2/FIXT.1.1 + 4.4 + 4.2 new repo ~204
fixparser (TS) Commercial Free tier needs a registered license key; encode + connectivity gated behind Pro (~$5K+/yr) Browser (partial) has deps ✅ ✅ Pro-only n/a multiple ~51 ~9,375
jspurefix (TS) Apache-2.0 Free Node-only has deps + heavy post-install ✅ ✅ ✅ (full session engine) n/a multiple ~75 ~61,370

For context — a different category, not embeddable analyzers: QuickFIX (C++) and QuickFIX/J (Java) are server-side session/transport engines; simplefix (Python) is lightweight but unmaintained and Python-only; hosted decoders (FIXSIM, Esprow, and similar) are paste-in SaaS, not npm libraries. If you need a socket that maintains sequence numbers and heartbeats, you want a session engine — see What this is not.


Three reasons to trust it

1. It works in front of you. The block above is real captured output, not a sketch. The repeating group becomes an array of nested objects you can index into; numeric fields like MDEntryPx (270) and OrderQty (38) come back as typed numbers, not strings. Pure string / Uint8Array in, structured object out, over TextEncoder / TextDecoder — no Node-only APIs in the core, so the same engine runs in a browser tab and on a Node backend.

2. It is provably correct, and it never throws. parse and validate return findings as data — they never throw, hang, or crash, even on garbage input. BeginString (8), BodyLength (9), and CheckSum (10) are computed byte-accurately on encode, and repeating groups are reconstructed faithfully. Correctness is pinned by golden fixtures and a reference oracle for the market-data and session message sets, a decode → encode round-trip across all 93 FIX 4.4 and all 115 FIX 5.0 SP2 messages, a cross-engine check (FIXT frames encoded here are parsed and validated clean by quickfix-go's transport/application validator), and an adversarial robustness suite that asserts parse and validate never throw, hang, or crash on malformed input. CI runs on Node 18 / 20 / 22 and a browser-like environment, and a bundle check enforces the zero-dependency, browser-safe surface on every commit.

3. It is enterprise-safe. Zero runtime dependencies — nothing to vet transitively, no heavy post-install, nothing that breaks the browser build. No telemetry. No license key, no registration, no gated "Pro" tier. Apache-2.0, with its explicit patent grant. The engine is pure and deterministic — no wall clock, no randomness, no global state — and it never invents session fields (you supply sequence numbers and timestamps), which is exactly why it is trivially testable and safe to embed.


Install

You need the engine plus at least one dictionary:

npm install @boarteam/fix @boarteam/fix-dict-fix44
# or
pnpm add @boarteam/fix @boarteam/fix-dict-fix44

Swap in @boarteam/fix-dict-fix42 for FIX 4.2, or @boarteam/fix-dict-fix50sp2 for FIX 5.0 SP2 over FIXT.1.1 (with @boarteam/fix-dict-fixt11 as the transport-only dictionary for layer-attributed validation). The dictionary packages declare a peer dependency on @boarteam/fix. Requirements: Node ≥ 18, or any modern browser. Both are zero-dependency, dual ESM + CJS, with TypeScript types included.


Documentation

The full documentation lives at boar.team/fix/docs — every code sample on it is compiled and executed against the released package at build time, so the docs cannot drift from the library.

Guide What it covers
Parse parse / parseAll, raw vs typed values, repeating groups, pipe-delimited logs, never-throws
Validate The FixIssue shape, stable issue codes as the SemVer contract, severities, layers
Encode Tag-keyed encode, dictionary field order, byte-accurate BodyLength / CheckSum
Typed messages The message() builder, compile-error safety, render(), immutability, type guards
Inbound messages toInbound, the name-keyed body and the session envelope, switch (msgType) dispatch
Extend a dictionary Venue-custom tags with defineExtension — cTrader's 1007/1008 as the worked example
Dictionaries Choosing FIX 4.4, 4.2 or 5.0 SP2, the FIXT pair API, free functions and output shapes

Alongside the guides:

  • API reference — every export, signature by signature, generated from the dist/api.json model this package ships (see docs/api-json.md), with source links and a "since" badge per export.
  • Diagnostics catalogue — every parse/*, validate/*, dict/* and extend/* issue code the engine can emit, each with its usual cause. Build-time checked against the installed package, so it is never out of date.
  • Playground — paste a raw message and decode it in the browser, running this engine client-side.
  • FIX reference — the browsable dictionary behind it all: tags, messages, components, datatypes, and per-dialect views for FIX 4.4, FIX 4.2 and FIX 5.0 SP2.

The rest of this README is the short tour.


Usage

Create an engine

import { createFixEngine } from '@boarteam/fix';
import { dictionary } from '@boarteam/fix-dict-fix44';

const fix = createFixEngine(dictionary);
// fix: { dictionary, parse, parseAll, encode, validate }

Parse

parse accepts a string or Uint8Array and never throws — it returns { message, issues }.

const { message, issues } = fix.parse(raw);

message.msgType; // "8"
message.name; // "ExecutionReport"
message.beginString; // "FIX.4.4"
message.framed; // true

// Typed values where the dictionary says so:
message.fields[38].value; // 1000000   OrderQty  (typed number, not "1000000")
message.fields[6].value; // 1.0921    AvgPx     (typed number)
message.fields[55].value; // "EUR/USD" Symbol

// Each field keeps the verbatim wire value as the source of truth for re-encode:
message.fields[55].raw; // "EUR/USD"

// Repeating groups are arrays of nested objects:
for (const entry of message.groups[268] ?? []) {
  console.log(entry.fields[269]?.value, entry.fields[270]?.value);
}

Use parseAll for a stream of concatenated messages (e.g. a log file).

→ Full guide: Parsing FIX messages.

Validate

validate checks presence (required fields), enum membership, datatypes, and conditional-required rules against the dictionary, returning a FixIssue[]:

const problems = fix.validate(message);
for (const issue of problems) {
  console.log(issue.severity, issue.code, issue.message, issue.path);
}

Every FixIssue carries a stable code (e.g. validate/value-not-in-enum, parse/checksum-mismatch), a severity (error | warning | info), a human message, and — where relevant — a path, refTagID, refSeqNum, refMsgType, or sessionRejectReason. The codes are part of the SemVer contract; the human message is not.

→ Full guide: Validating FIX messages. Every issue code the engine can emit is catalogued, with its usual cause, in the diagnostics reference.

Encode

import { MsgType, Tags } from '@boarteam/fix-dict-fix44';

const wire = fix.encode({
  msgType: MsgType.NewOrderSingle,
  fields: {
    [Tags.SenderCompID]: 'BUYSIDE',
    [Tags.TargetCompID]: 'SELLSIDE',
    [Tags.MsgSeqNum]: 42,
    [Tags.SendingTime]: '20240101-12:00:00.000',
    [Tags.ClOrdID]: 'ORDER-1',
    [Tags.Symbol]: 'EUR/USD',
    [Tags.Side]: '1',
    [Tags.TransactTime]: '20240101-12:00:00.000',
    [Tags.OrderQty]: 1_000_000,
    [Tags.OrdType]: '2',
    [Tags.Price]: 1.0921,
  },
});

encode emits fields in dictionary order and computes BeginString (8), BodyLength (9), and CheckSum (10) byte-accurately. It is pure — you supply the session fields (sequence number, sending time); the engine never invents them.

→ Full guide: Encoding FIX messages.

Typed messages

encode above is the untyped, tag-keyed primitive. For a statically-typed encode side, each dictionary package also ships a message factory generated from the same dictionary: creating a message for a MsgType yields a builder that knows only that message's fields/groups and their value types, and renders byte-identical to encode.

import { message, MsgType } from '@boarteam/fix-dict-fix44';

const wire = message(MsgType.MarketDataSnapshotFullRefresh) // typed to this message's body
  .set('MDReqID', 'req-1')
  .set('Symbol', 'EUR/USD')
  .set('NoMDEntries', [
    { MDEntryType: '0', MDEntryPx: '1.1050' }, // group entries are typed too
    { MDEntryType: '1', MDEntryPx: '1.1052' },
  ])
  .render({
    SenderCompID: 'BUYSIDE',
    TargetCompID: 'SELLSIDE',
    MsgSeqNum: 42,
    SendingTime: '20240101-12:00:00.000',
  });

Illegal fields, wrong value types, and malformed group entries are compile errors; value types follow the field datatype (enumerated → the value union; Boolean → boolean; numeric → number | string; else string). Envelope/session fields (MsgSeqNum/SenderCompID/SendingTime/TargetCompID; framing 8/9/10 computed) are supplied to render — the body type excludes them, so the library holds no sequence counter, clock, or comp-IDs. message(...) is a fast, fluent mutable builder for hot loops; message.immutable(...) is copy-on-write; both accept a bulk object and double as a typed read model (msg.get('Symbol')). The engine façade mirrors it: createFixEngine<MessageBodies>(dictionary).create(msgType). Venue-custom tags need no regenerated dictionary — see Extending the dictionary and each package's README.

Where the concrete type is erased — a generic send(message: MessageView<any>), a log-metadata helper — message.msgType === 'W' cannot narrow the body, so each dictionary package also ships isMessageType, a guard keyed on the MsgType value (and a MessageOf<M> alias for annotations), plus isInboundType/InboundOf<M>, the same pair for a message that arrived rather than one you built. Inside it, reads are typed to that message with no casts; the runtime is a plain string compare, the typing comes from the generated body registry. The engine binds the same guard as createFixEngine<MessageBodies>(dictionary).is.

import { isMessageType, MsgType } from '@boarteam/fix-dict-fix44';

if (isMessageType(message, MsgType.MarketDataSnapshotFullRefresh)) {
  const securityID = message.get('SecurityID'); // string | undefined
  for (const entry of message.get('NoMDEntries') ?? []) {
    entry.MDEntryPx; // MDFullGrp_NoMDEntriesEntry — entry fields typed
  }
}

→ Full guide: Typed messages.

Inbound messages

The same per-message types work on the way in. parse above returns the wire faithfully but tag-keyed (message.fields[262].value, groups under message.groups[268]) — the right shape for a codec, the wrong one for application code, which ends up hand-writing a switch (msgType) plus a tag-to-field mapper per message. toInbound re-keys it by dictionary name, splits the standard header/trailer into a typed envelope, and hands back a read model that narrows per MsgType.

import { inboundKnownGuard, loadDictionary, parse, toInbound } from '@boarteam/fix';
import { dictionary as fix44, MsgType, type MessageBodies } from '@boarteam/fix-dict-fix44';

// The dict packages ship the dictionary as data; `parse` wants it indexed.
const dictionary = loadDictionary(fix44);
const isKnownInbound = inboundKnownGuard<MessageBodies>(dictionary);

const { message, issues } = parse(raw, dictionary); // gate on `issues` first
const inbound = toInbound(message, dictionary);

inbound.envelope.MsgSeqNum; // session envelope, before narrowing

if (isKnownInbound(inbound)) {
  switch (
    inbound.msgType // narrows per case
  ) {
    case MsgType.Logon:
      inbound.get('HeartBtInt'); // typed to LogonBody
      break;
    case MsgType.MarketDataSnapshotFullRefresh:
      inbound.get('NoMDEntries')?.[0]?.MDEntryPx; // the typed entry array
      break;
    default:
      break; // known, but not handled here
  }
}

Repeating groups are arrays of entry objects under their counter's name — the declared counter is not a property, because the entry array is the count. Header/trailer fields land on envelope, matching the body types, which exclude them by construction. The isKnownInbound guard is not ceremony: an unrecognised MsgType cannot be a member of the message union (a msgType: string member overlaps every literal, so no case eliminates it and its loose get makes every branch uncallable), and it does not parse like one either — it is read flat, with no groups reconstructed. inboundTypeGuard is the single-MsgType if form — each dictionary package ships it ready-bound as isInboundType, so binding it yourself is only necessary for a dictionary you extended. Nothing is re-parsed: toInbound is a pure re-keying, and the original ParsedMessage stays reachable as inbound.parsed for byte-faithful re-encoding. The engine façade binds .inbound(), .isInbound and .isKnown. Runnable: examples/read-a-message.mjs.

→ Full guide: Inbound messages.

Reading pipe-delimited logs

Most captured logs render the SOH separator as |. Pass it through:

const { message, issues } = fix.parse(pipeDelimitedLine, { soh: '|' });

parse options: soh (default the SOH byte 0x01; pass "|" to read pipe-delimited logs) and checkFraming (default true; framing findings come back as issues, never a rejection).

Extending the dictionary (venue-custom tags)

Real venues extend FIX beyond the spec — cTrader, for example, sends SymbolName(1007) and SymbolDigits(1008) inside the NoRelatedSym repeating group of SecurityList (35=y). Unknown tags inside a group break group reconstruction, and encode only emits fields the dictionary places. extendDictionary fixes both from one declaration:

import { defineExtension, extendDictionary, createFixEngine } from '@boarteam/fix';
import { dictionary as fix44 } from '@boarteam/fix-dict-fix44';

const ctrader = defineExtension({
  id: 'ctrader',
  fields: {
    SymbolName: { tag: 1007, type: 'String' },
    SymbolDigits: { tag: 1008, type: 'int' },
  },
  messages: {
    SecurityList: { groups: { NoRelatedSym: { append: ['SymbolName', 'SymbolDigits'] } } },
  },
});

const { dictionary, issues } = extendDictionary(fix44, ctrader); // never throws — issues are data
const fix = createFixEngine(dictionary);
// SecurityList now parses with 1007/1008 nested per instrument, and encode round-trips them.

An extension can add fields, enum values on existing fields, components, whole messages (the standard header/trailer are injected for you), and place members into message bodies or repeating groups — after: 'Instrument' anchors a wire position, dotted paths ('NoRelatedSym.NoUnderlyings') reach nested groups. Placements are append-only by design: a group's first field is its entry delimiter, and extendDictionary reverts any operation that would shift one (extend/group-delimiter-shift), skips duplicates, and rejects placements that could re-parse into the wrong group (extend/ambiguous-boundary). Everything it does or refuses to do is reported through stable extend/* issue codes — error means skipped or reverted, so if the base passed validateDictionary and the result has no error-severity issues, the result passes too.

The same declaration drives the typed maps — no duplication, full literal typing (TS ≥ 5.0):

import { extendTags, invertTags, tagsOf } from '@boarteam/fix';
import { Tags as Fix44Tags } from '@boarteam/fix-dict-fix44';

export const Tags = extendTags(Fix44Tags, tagsOf(ctrader)); // Tags.SymbolName hovers as 1007
export type TagName = keyof typeof Tags; // includes 'SymbolName'
export const TagNames = invertTags(Tags); // TagNames[1007] hovers as 'SymbolName'

For a reusable dialect, put the declaration and the derived exports in one module mirroring a dict package's surface (dictionary, Tags, TagNames, MsgType, MsgTypeNames) — see the runnable examples/extend-dictionary.mjs. Two things keep such a module honest: fail fast on error-severity issues at module init, and — if the module ships as a package that emits .d.ts — annotate the exports with the provided alias types (ExtendTags, InvertTags, ExtendMsgTypes, InvertMsgTypes) so the declaration emit stays small and nominal instead of structurally inlining the 900-key base map. The same pattern scales to publishable venue packages (e.g. a future @boarteam/fix-dict-fix44-ctrader).

→ Full guide: Extending a dictionary.

Lower-level building blocks

If you do not want the engine wrapper, the same capabilities are exported as free functions: parse, parseAll, encode, validate, tokenize, splitMessages, decodeValue, calculateChecksum, bodyLength, loadDictionary, Dictionary, validateDictionary, the typed-message helpers (messageFactory, messageTypeGuard, createMessage, createImmutableMessage, toInbound, inboundTypeGuard, inboundKnownGuard), and the dictionary-extension helpers (extendDictionary, defineExtension, tagsOf, msgTypesOf, extendTags, invertTags, extendMsgTypes, invertMsgTypes).

Every one of them is documented, signature by signature, in the API reference.

Output shapes

  • ParsedMessage — { msgType, name?, beginString?, framed, fields, groups }.
  • ParsedField — { tag, name?, raw, value }. raw is the verbatim wire value (the source of truth for re-encoding); value is the typed value.
  • ParsedGroupEntry — { fields, groups }. Repeating groups are arrays of nested objects, addressed by their counter tag (e.g. message.groups[268]), not parallel arrays.
  • FixIssue — { code, severity, message, path?, refTagID?, refSeqNum?, refMsgType?, sessionRejectReason? }.

Why zero dependencies matters

For a library that sits in a trading firm's toolchain, the dependency tree is the attack surface and the audit burden. @boarteam/fix has no runtime dependencies — nothing to vet transitively, nothing to phone home, nothing that breaks the browser build. A CI bundle check enforces this on every commit, so the zero-dependency, browser-safe surface cannot silently regress.


Coverage & correctness

The dictionaries ship as data, generated by the separate @boarteam/fix-codegen generator from permissively-licensed sources. The counts below are read directly from the shipped dictionary files, and each dialect is browsable field-by-field on the FIX reference — the coverage page renders the declared gaps below from the same shipped data.

FIX 4.4 — @boarteam/fix-dict-fix44

Fields Messages Components Datatypes Inline repeating groups
912 93 105 23 92

Generated directly from the QuickFIX FIX.4.4 XML data dictionary. Because the 4.4 structure is generated from QuickFIX, we do not claim it is "cross-checked against QuickFIX" — that would be circular. There are 36 recorded coverage gaps, all of one kind: conditional-overlay-unmatched — conditional-required rules from the facts overlay that did not match a member in the QuickFIX-sourced structure. They are recorded as data in the dictionary, naming exactly what they touch; they are not parsing failures and do not affect the market-data or session message sets.

FIX 4.2 — @boarteam/fix-dict-fix42

Fields Messages Components Datatypes
405 46 2 21

Generated from the FIX Repository 2010 Edition and genuinely cross-checked against the QuickFIX FIX42.xml dictionary by a CI drift gate. There are 2 recorded coverage gaps, both synthesized-datatype: MultipleValueString (synthesized over base String) and Length (synthesized over base int) are each referenced by fields but absent from Datatypes.xml.

FIX 5.0 SP2 over FIXT.1.1 — @boarteam/fix-dict-fix50sp2

Fields Messages Components Datatypes Inline repeating groups
1,452 115 176 28 147

The self-contained FIX 5.0 SP2 dictionary: the FIXT.1.1 session envelope + the 7 session messages + the 108 base-SP2 application messages, with version: 'FIX.5.0SP2', beginString: 'FIXT.1.1', and applVerID: '9' modelling the transport/application split FIX 5.0 introduced. Generated from the QuickFIX/J FIX50SP2.xml + FIXT11.xml data dictionaries (base SP2 — deliberately not the quickfix C++ copy, which was regenerated at EP280) and cross-checked against quickfix-go's independently-maintained FIX50SP2.xml by a CI drift gate with a reviewed EP-drift baseline. Descriptions come from FIX Orchestra (EP307 application layer + EP247 session layer; 99.9% of fields, 99.7% of enum values — the remainder is recorded in coverageGaps). Golden FIXT frames encoded by this engine are parsed and validated clean by quickfix-go's transport/application validator (cross-engine check). Known limitations: XMLnonFIX(n) is not shipped (QuickFIX/J master comments it out; quickfix-go omits it), and like FIX 4.2 there are no conditional-required (C) markings.

FIXT.1.1 — @boarteam/fix-dict-fixt11

Fields Messages Components Datatypes
74 7 4 9

The transport-only dictionary: the FIXT session envelope (a strict superset of the FIX 4.4 header — adds ApplVerID(1128), CstmApplVerID(1129), ApplExtID(1156), HopGrp) and the session messages, with Logon requiring DefaultApplVerID(1137) — in the data and in the generated LogonBody type. Pair it with an application dictionary (createFixEngine({ transport, app })) for layer-attributed validation: every finding is tagged session or application, so you know whether to answer with a session Reject(3) or a BusinessMessageReject(j).

Field, enum, and datatype descriptions are sourced from the Apache-2.0 FIX Orchestra files.


What this is not

@boarteam/fix is an analyzer, deliberately not a session/transport engine. It does not — and will not — open sockets, manage sequence numbers, send heartbeats, handle logon/logout, or persist session state. Those belong to a session engine (QuickFIX, QuickFIX/J, jspurefix, and friends), and we cede that lane to them on purpose.

What it owns instead: decoding and validating messages you already have, encoding messages you construct, and doing all of it in a browser tab or a CI step. If your job is a log viewer, a web decode dashboard, a test harness, or a CI assertion, this is built for you. If your job is to be the FIX connection on the wire, reach for a session engine and let this analyze its traffic.


Packages

Package Version What it is
@boarteam/fix 0.4.0 The engine: tokenize / parse / validate / encode + the dictionary runtime + the FIXT pair API.
@boarteam/fix-dict-fix44 3.0.0 Full FIX 4.4 dictionary as data. Peer-depends on @boarteam/fix.
@boarteam/fix-dict-fix42 3.0.0 Full FIX 4.2 dictionary as data. Peer-depends on @boarteam/fix.
@boarteam/fix-dict-fix50sp2 1.0.0 FIX 5.0 SP2 over FIXT.1.1, self-contained (envelope + session + app messages).
@boarteam/fix-dict-fixt11 1.0.0 FIXT.1.1 transport-only dictionary (envelope + session messages), for the pair API.

All packages: zero runtime dependencies, dual ESM + CJS, Node ≥ 18, browser + Node, Apache-2.0. The dictionary packages also export dictionary, Tags (name → tag), MsgType (name → msgtype), and DICTIONARY_VERSION (e.g. "FIX.4.4", "FIX.5.0SP2").

Which dictionary to pick, and how the FIXT pair fits together: Dictionaries guide. To browse the data itself before installing anything, each dialect has a reference view — FIX 4.4, FIX 4.2, FIX 5.0 SP2 — and there is a 4.4 vs 4.2 diff.


Roadmap & stability

This is pre-1.0 software, and we are explicit about what that means rather than hiding it — because you have been burned by abandoned free FIX parsers before.

The SemVer contract covers three things:

  1. the output shape — ParsedMessage, FixIssue, and the dictionary JSON contract;
  2. the accepted input to parse / encode / validate;
  3. the set and meaning of issue codes (the stable code strings, e.g. parse/checksum-mismatch, validate/value-not-in-enum). The human-readable message is not part of the contract.

While on 0.x, a breaking change to any of those ships as a minor bump (0.x → 0.(x+1)); additive changes and fixes are patch bumps. Pin a version, and the issue codes you assert against will not silently change underneath you.

Maintenance pledge. A monthly release / maintenance cadence. Consistency is the whole point — the prior generation of free JS FIX parsers died from neglect, and that is exactly the failure mode this project is built to avoid. Issues and feedback shape the road to 1.0.

Roadmap. FIX 5.0 / FIXT.1.1 dictionaries through the same pipeline — shipped: @boarteam/fix-dict-fix50sp2 + @boarteam/fix-dict-fixt11, generated and cross-checked like the 4.x dicts, with the transport/application pair API in the engine. Next: a CLI and first-class FIX Orchestra support.


Development

This is a pnpm monorepo. Tests run on Node 18 / 20 / 22 plus a browser-like environment.

pnpm install
pnpm build          # build all packages (tsup -> ESM + CJS + d.ts)
pnpm test           # vitest: golden fixtures, round-trip, adversarial suite
pnpm typecheck      # tsc --noEmit across packages
pnpm check:bundle   # enforce the zero-dependency, browser-safe surface
pnpm examples       # run the runnable examples

The dictionary data is produced by the separate @boarteam/fix-codegen generator from QuickFIX / FIX Repository structure and Apache-2.0 FIX Orchestra descriptions.


Contributing

Contributions are welcome — see CONTRIBUTING.md for the development workflow, the DCO sign-off requirement, and how the FIX 4.2 cross-check drift gate works. Please also read our CODE_OF_CONDUCT.md. Security reports go through SECURITY.md. When changing parsing, validation, or encoding behavior, add or update a golden fixture so the change is provable.


License

Apache-2.0 © Boar Team.

Dictionary structure is derived from QuickFIX (QuickFIX Software License) and the FIX Repository (both permissively licensed); field, enum, and datatype descriptions come from the Apache-2.0 FIX Orchestra sources. Full attribution lives in NOTICE and THIRD-PARTY-NOTICES.txt. This project carries only permissively-licensed data and does not include CC BY-ND FIX-specification prose.

FIX is a trademark of FIX Protocol Limited; this is an independent project, not affiliated with or endorsed by FIX Protocol Limited.

About

Dictionary-driven, zero-dependency FIX protocol toolkit for TypeScript (browser + Node).

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

19 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages