Skip to content

Commit adaac14

Browse files
hsc-jyskebank-dkHenrik Christensen
authored andcommitted
Add FIX JSON Encoding serialization (ToJSON/FromJSON)
Implement Message.ToJSON and Message.FromJSON following the FIX JSON Encoding specification (https://github.com/FIXTradingCommunity/fix-json-encoding-spec) and modeled after the equivalent implementation in QuickFIX/n. ToJSON serializes a Message into the canonical JSON envelope: {"Header": {...}, "Body": {...}, "Trailer": {...}} When a DataDictionary is provided, field tags are emitted as human-readable names and repeating groups are represented as JSON arrays of objects. Without a DataDictionary, numeric tag strings are used as keys. CheckSum (tag 10) is always excluded from the output per the spec. FromJSON parses a FIX JSON message back into a Message. Named keys are resolved to FIX tags via the DataDictionary; numeric tag strings are also accepted. Repeating groups are reconstructed from JSON arrays.
1 parent 236c791 commit adaac14

2 files changed

Lines changed: 609 additions & 0 deletions

File tree

message_json.go

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
// Copyright (c) quickfixengine.org All rights reserved.
2+
//
3+
// This file may be distributed under the terms of the quickfixengine.org
4+
// license as defined by quickfixengine.org and appearing in the file
5+
// LICENSE included in the packaging of this file.
6+
//
7+
// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
8+
// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
9+
// PARTICULAR PURPOSE.
10+
//
11+
// See http://www.quickfixengine.org/LICENSE for licensing information.
12+
//
13+
// Contact ask@quickfixengine.org if any conditions of this licensing
14+
// are not clear to you.
15+
16+
package quickfix
17+
18+
import (
19+
"bytes"
20+
"encoding/json"
21+
"fmt"
22+
"strconv"
23+
24+
"github.com/quickfixgo/quickfix/datadictionary"
25+
)
26+
27+
// ToJSON serializes the Message to the FIX JSON Encoding format.
28+
// See: https://github.com/FIXTradingCommunity/fix-json-encoding-spec
29+
//
30+
// If dd is nil, numeric tag numbers are used as field names and repeating groups
31+
// are serialized as a flat string rather than as JSON arrays.
32+
func (m *Message) ToJSON(dd *datadictionary.DataDictionary) ([]byte, error) {
33+
msgType, _ := m.MsgType()
34+
35+
var headerFields, bodyFields, trailerFields map[int]*datadictionary.FieldDef
36+
if dd != nil {
37+
if dd.Header != nil {
38+
headerFields = dd.Header.Fields
39+
}
40+
if dd.Trailer != nil {
41+
trailerFields = dd.Trailer.Fields
42+
}
43+
if msgType != "" {
44+
if msgDef, ok := dd.Messages[msgType]; ok {
45+
bodyFields = msgDef.Fields
46+
}
47+
}
48+
}
49+
50+
var buf bytes.Buffer
51+
buf.WriteString(`{"Header":`)
52+
writeFieldMapJSON(&buf, &m.Header.FieldMap, headerFields, dd)
53+
buf.WriteString(`,"Body":`)
54+
writeFieldMapJSON(&buf, &m.Body.FieldMap, bodyFields, dd)
55+
buf.WriteString(`,"Trailer":`)
56+
writeFieldMapJSON(&buf, &m.Trailer.FieldMap, trailerFields, dd)
57+
buf.WriteByte('}')
58+
59+
return buf.Bytes(), nil
60+
}
61+
62+
// FromJSON populates the Message from a FIX JSON Encoding byte slice.
63+
// See: https://github.com/FIXTradingCommunity/fix-json-encoding-spec
64+
//
65+
// If dd is nil, field names must be numeric tag numbers and repeating groups
66+
// are not supported.
67+
func (m *Message) FromJSON(data []byte, dd *datadictionary.DataDictionary) error {
68+
var raw struct {
69+
Header json.RawMessage `json:"Header"`
70+
Body json.RawMessage `json:"Body"`
71+
Trailer json.RawMessage `json:"Trailer"`
72+
}
73+
if err := json.Unmarshal(data, &raw); err != nil {
74+
return fmt.Errorf("error parsing FIX JSON message: %w", err)
75+
}
76+
77+
var headerFields map[int]*datadictionary.FieldDef
78+
if dd != nil && dd.Header != nil {
79+
headerFields = dd.Header.Fields
80+
}
81+
82+
if err := populateFieldMapFromJSON(&m.Header.FieldMap, raw.Header, headerFields, dd); err != nil {
83+
return fmt.Errorf("error parsing FIX JSON Header: %w", err)
84+
}
85+
86+
// Parse MsgType from the header to determine the body field context.
87+
msgType, _ := m.MsgType()
88+
89+
var trailerFields, bodyFields map[int]*datadictionary.FieldDef
90+
if dd != nil {
91+
if dd.Trailer != nil {
92+
trailerFields = dd.Trailer.Fields
93+
}
94+
if msgType != "" {
95+
if msgDef, ok := dd.Messages[msgType]; ok {
96+
bodyFields = msgDef.Fields
97+
}
98+
}
99+
}
100+
101+
if err := populateFieldMapFromJSON(&m.Body.FieldMap, raw.Body, bodyFields, dd); err != nil {
102+
return fmt.Errorf("error parsing FIX JSON Body: %w", err)
103+
}
104+
if err := populateFieldMapFromJSON(&m.Trailer.FieldMap, raw.Trailer, trailerFields, dd); err != nil {
105+
return fmt.Errorf("error parsing FIX JSON Trailer: %w", err)
106+
}
107+
108+
return nil
109+
}
110+
111+
// writeFieldMapJSON writes a FieldMap's fields as a JSON object into buf.
112+
// contextFields (if non-nil) identifies which tags represent repeating groups in this message context.
113+
func writeFieldMapJSON(buf *bytes.Buffer, fm *FieldMap, contextFields map[int]*datadictionary.FieldDef, dd *datadictionary.DataDictionary) {
114+
fm.rwLock.Lock()
115+
defer fm.rwLock.Unlock()
116+
117+
buf.WriteByte('{')
118+
first := true
119+
120+
for _, tag := range fm.sortedTags() {
121+
if tag == tagCheckSum {
122+
continue
123+
}
124+
125+
f, ok := fm.tagLookup[tag]
126+
if !ok {
127+
continue
128+
}
129+
130+
name := fieldNameOrTag(tag, dd)
131+
132+
var groupDef *datadictionary.FieldDef
133+
if contextFields != nil {
134+
if fd, ok2 := contextFields[int(tag)]; ok2 && fd.IsGroup() {
135+
groupDef = fd
136+
}
137+
}
138+
139+
if !first {
140+
buf.WriteByte(',')
141+
}
142+
first = false
143+
144+
// Write JSON key.
145+
nameBytes, _ := json.Marshal(name)
146+
buf.Write(nameBytes)
147+
buf.WriteByte(':')
148+
149+
if groupDef != nil {
150+
writeGroupJSON(buf, f, groupDef, dd)
151+
} else {
152+
valBytes, _ := json.Marshal(string(f[0].value))
153+
buf.Write(valBytes)
154+
}
155+
}
156+
157+
buf.WriteByte('}')
158+
}
159+
160+
// writeGroupJSON serializes a repeating-group field as a JSON array of objects.
161+
// f is the raw field slice stored in the FieldMap (count tag + all group member tags).
162+
func writeGroupJSON(buf *bytes.Buffer, f field, groupDef *datadictionary.FieldDef, dd *datadictionary.DataDictionary) {
163+
template := buildGroupTemplate(groupDef)
164+
rg := NewRepeatingGroup(Tag(groupDef.Tag()), template)
165+
_, _ = rg.Read(f)
166+
167+
subFields := groupSubFields(groupDef)
168+
169+
buf.WriteByte('[')
170+
for i, grp := range rg.groups {
171+
if i > 0 {
172+
buf.WriteByte(',')
173+
}
174+
writeFieldMapJSON(buf, &grp.FieldMap, subFields, dd)
175+
}
176+
buf.WriteByte(']')
177+
}
178+
179+
// populateFieldMapFromJSON reads fields from a JSON object and sets them on fm.
180+
// contextFields (if non-nil) identifies which tags are repeating groups in this context.
181+
func populateFieldMapFromJSON(fm *FieldMap, data json.RawMessage, contextFields map[int]*datadictionary.FieldDef, dd *datadictionary.DataDictionary) error {
182+
if len(data) == 0 {
183+
return nil
184+
}
185+
186+
var rawFields map[string]json.RawMessage
187+
if err := json.Unmarshal(data, &rawFields); err != nil {
188+
return err
189+
}
190+
191+
for name, rawValue := range rawFields {
192+
tag, err := resolveTag(name, dd)
193+
if err != nil {
194+
continue // skip unknown fields
195+
}
196+
197+
// JSON arrays represent repeating groups.
198+
if len(rawValue) > 0 && rawValue[0] == '[' {
199+
var groupDef *datadictionary.FieldDef
200+
if contextFields != nil {
201+
if fd, ok := contextFields[int(tag)]; ok && fd.IsGroup() {
202+
groupDef = fd
203+
}
204+
}
205+
if groupDef == nil {
206+
continue // skip groups we cannot parse without a definition
207+
}
208+
209+
var items []json.RawMessage
210+
if err := json.Unmarshal(rawValue, &items); err != nil {
211+
return fmt.Errorf("error parsing group %q: %w", name, err)
212+
}
213+
214+
subFields := groupSubFields(groupDef)
215+
rg := NewRepeatingGroup(tag, buildGroupTemplate(groupDef))
216+
for _, item := range items {
217+
grp := rg.Add()
218+
if err := populateFieldMapFromJSON(&grp.FieldMap, item, subFields, dd); err != nil {
219+
return err
220+
}
221+
}
222+
fm.SetGroup(rg)
223+
} else {
224+
var value string
225+
if err := json.Unmarshal(rawValue, &value); err != nil {
226+
return fmt.Errorf("error parsing field %q value: %w", name, err)
227+
}
228+
fm.SetBytes(tag, []byte(value))
229+
}
230+
}
231+
232+
return nil
233+
}
234+
235+
// buildGroupTemplate constructs a GroupTemplate from a group FieldDef, handling nested groups recursively.
236+
func buildGroupTemplate(groupDef *datadictionary.FieldDef) GroupTemplate {
237+
template := make(GroupTemplate, 0, len(groupDef.Fields))
238+
for _, childDef := range groupDef.Fields {
239+
if childDef.IsGroup() {
240+
template = append(template, NewRepeatingGroup(Tag(childDef.Tag()), buildGroupTemplate(childDef)))
241+
} else {
242+
template = append(template, GroupElement(Tag(childDef.Tag())))
243+
}
244+
}
245+
return template
246+
}
247+
248+
// groupSubFields builds a tag→FieldDef map for the direct children of a group FieldDef.
249+
func groupSubFields(groupDef *datadictionary.FieldDef) map[int]*datadictionary.FieldDef {
250+
sub := make(map[int]*datadictionary.FieldDef, len(groupDef.Fields))
251+
for _, fd := range groupDef.Fields {
252+
sub[fd.Tag()] = fd
253+
}
254+
return sub
255+
}
256+
257+
// fieldNameOrTag returns the human-readable field name from the data dictionary,
258+
// or the numeric tag string when the dictionary is absent or the tag is unknown.
259+
func fieldNameOrTag(tag Tag, dd *datadictionary.DataDictionary) string {
260+
if dd != nil {
261+
if ft, ok := dd.FieldTypeByTag[int(tag)]; ok {
262+
return ft.Name()
263+
}
264+
}
265+
return strconv.Itoa(int(tag))
266+
}
267+
268+
// resolveTag maps a JSON field name to a FIX Tag.
269+
// It first checks the data dictionary by name, then falls back to parsing a numeric string.
270+
func resolveTag(name string, dd *datadictionary.DataDictionary) (Tag, error) {
271+
if dd != nil {
272+
if ft, ok := dd.FieldTypeByName[name]; ok {
273+
return Tag(ft.Tag()), nil
274+
}
275+
}
276+
n, err := strconv.Atoi(name)
277+
if err != nil {
278+
return 0, fmt.Errorf("unknown field %q", name)
279+
}
280+
return Tag(n), nil
281+
}

0 commit comments

Comments
 (0)