Encoding FIX messages

encode turns { msgType, fields, groups } into a complete, framed FIX string. You describe the content; the dictionary decides the order; the engine does the framing math. The result parses back clean by construction — the round trip is on this page, executed.

Dictionary order, computed framing

Fields are keyed by tag — the dictionary packages export a Tags map so call sites stay readable. However the input object is arranged, fields are emitted in the order the dictionary prescribes for that message (components and repeating groups expanded in place), and BeginString (8), BodyLength (9) and CheckSum (10) are computed byte-accurately — UTF-8 aware, so non-ASCII values still frame correctly.

encode-order.mtsts
import { createFixEngine } from '@boarteam/fix';import { dictionary, MsgType, Tags } from '@boarteam/fix-dict-fix44';const fix = createFixEngine(dictionary);// Fields are keyed by tag — the Tags map keeps that readable. The order of// THIS object does not matter: encode emits the dictionary's field order for// NewOrderSingle, then computes BeginString (8), BodyLength (9) and// CheckSum (10) byte-accurately. { soh: '|' } frames with pipes for// readability; omit it to emit the real SOH byte.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,    },  },  { soh: '|' },);console.log(wire);/* →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|*/// The framing math proves itself: the output parses back with zero issues.const back = fix.parse(wire, { soh: '|' });console.log(back.issues.length); // → 0console.log(back.message.fields[44].value); // → 1.0921

encode throws only on programming errors — an unknown msgType, or a number that would render in exponent notation (pass such values pre-formatted as strings). Absent fields never throw.

Pure: the caller owns the session

The engine reads a wall clock nowhere, keeps no sequence counter and stores no comp-IDs — that is the session engine’s job, and this library is deliberately not one. MsgSeqNum, SendingTime and the comp-IDs are ordinary fields you supply. Forget them and encode still frames exactly what you gave it; validate is what tells you the message is not sendable yet — which is precisely the division of labour a test harness wants.

encode-pure.mtsts
import { createFixEngine } from '@boarteam/fix';import { dictionary, MsgType, Tags } from '@boarteam/fix-dict-fix44';const fix = createFixEngine(dictionary);// encode is PURE: no wall clock, no sequence counter, no comp-ID state.// Session fields are ordinary fields that YOU supply — omit them and encode// still frames exactly what it was given, without inventing anything:const wire = fix.encode(  {    msgType: MsgType.NewOrderSingle,    fields: {      [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,    },  },  { soh: '|' },);console.log(wire);/* →8=FIX.4.4|9=83|35=D|11=ORDER-1|55=EUR/USD|54=1|60=20240101-12:00:00.000|38=1000000|40=2|44=1.0921|10=011|*/// Whether the result is SENDABLE is validate's job, and it names exactly// what the session layer still owes this message:const { message } = fix.parse(wire, { soh: '|' });for (const problem of fix.validate(message)) {  console.log(problem.code, problem.path);}// → validate/required-field-missing SenderCompID// → validate/required-field-missing TargetCompID// → validate/required-field-missing MsgSeqNum// → validate/required-field-missing SendingTime

One sharp edge worth knowing: a field supplied at the wrong nesting level — a group member passed as a top-level field, say — is silently dropped, because the dictionary has no position for it there. The re-parse in the sample above is the honest check: assert on what came back, not on what you sent.

Prefer compile-time safety?

Tag-keyed encode is the untyped primitive: maximally flexible, checked at runtime by validate. Each dictionary package also generates a message() builder that knows exactly one message’s fields and value types, catches mistakes at compile time, and renders byte-identical wire — see typed messages.