diff --git a/Examples/JsonToFix/Examples.JsonToFix.csproj b/Examples/JsonToFix/Examples.JsonToFix.csproj new file mode 100644 index 000000000..22756adfd --- /dev/null +++ b/Examples/JsonToFix/Examples.JsonToFix.csproj @@ -0,0 +1,16 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + diff --git a/Examples/JsonToFix/Program.cs b/Examples/JsonToFix/Program.cs new file mode 100644 index 000000000..e33c3bee8 --- /dev/null +++ b/Examples/JsonToFix/Program.cs @@ -0,0 +1,77 @@ +using System; +using System.Text; +using System.Text.Json; +using System.IO; +using QuickFix; + +namespace TradeClient +{ + class Program + { + static void JsonMsgToFix(string json, QuickFix.DataDictionary.DataDictionary sessionDataDictionary, QuickFix.DataDictionary.DataDictionary appDataDictionary, QuickFix.IMessageFactory msgFactory) + { + var msg = new Message(); + msg.FromJson(json, true, sessionDataDictionary, appDataDictionary, msgFactory); + Console.WriteLine(msg.ToString()); + } + + static void JsonToFix(string fname, QuickFix.DataDictionary.DataDictionary sessionDataDictionary, QuickFix.DataDictionary.DataDictionary appDataDictionary) + { + try + { + QuickFix.IMessageFactory msgFactory = new QuickFix.DefaultMessageFactory(); + string json = File.ReadAllText(fname); + using (JsonDocument document = JsonDocument.Parse(json)) + { + if (document.RootElement.TryGetProperty("messages", out JsonElement messagesElement)) + { + foreach (JsonElement jsonMsg in messagesElement.EnumerateArray()) + { + JsonMsgToFix(jsonMsg.ToString(), sessionDataDictionary, appDataDictionary, msgFactory); + } + } + else // assume there is only one message instead of an array of messages + { + JsonMsgToFix(json, sessionDataDictionary, appDataDictionary, msgFactory); + } + } + } + catch (System.Exception e) + { + Console.WriteLine(e.Message); + Console.WriteLine(e.StackTrace); + } + } + + [STAThread] + static void Main(string[] args) + { + if (args.Length < 1 || args.Length > 2) + { + System.Console.WriteLine("USAGE"); + System.Console.WriteLine(""); + System.Console.WriteLine(" FixToJson.exe FILE DATA_DICTIONARY"); + System.Console.WriteLine(""); + System.Console.WriteLine(" The FILE may contain either a single message in FIX JSON Encoding, or an array of messages in a root-level \"messages\" element."); + System.Console.WriteLine(""); + System.Console.WriteLine("EXAMPLES"); + System.Console.WriteLine(""); + System.Console.WriteLine(" JsonToFix.exe messages.json ../../spec/fix/FIX50SP2.xml"); + System.Console.WriteLine(" JsonToFix.exe messages.json ../../spec/fix/FIX44.xml"); + System.Console.WriteLine(" JsonToFix.exe messages.json ../../spec/fix/FIX42.xml"); + System.Console.WriteLine(""); + System.Console.WriteLine("NOTE"); + System.Console.WriteLine(""); + System.Console.WriteLine(" Per the FIX JSON Encoding Specification, tags are converted to human-readable form, but values are not."); + System.Environment.Exit(2); + } + + string fname = args[0]; + QuickFix.DataDictionary.DataDictionary sessionDataDictionary = new QuickFix.DataDictionary.DataDictionary(args[1]); + QuickFix.DataDictionary.DataDictionary appDataDictionary = sessionDataDictionary; + + JsonToFix(fname, sessionDataDictionary, appDataDictionary); + Environment.Exit(1); + } + } +} diff --git a/QuickFIXn.sln b/QuickFIXn.sln index 99eb3b78c..af62f1e7b 100644 --- a/QuickFIXn.sln +++ b/QuickFIXn.sln @@ -50,6 +50,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickFix.FIX50SP2", "Messag EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickFix.FIXT11", "Messages\FIXT11\QuickFix.FIXT11.csproj", "{6EABF160-E21A-4ABD-82C9-0DCC085BDE07}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.JsonToFix", "Examples\JsonToFix\Examples.JsonToFix.csproj", "{68D01488-2B63-450C-A0D0-C6426C9E4AE7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -178,6 +180,14 @@ Global {6EABF160-E21A-4ABD-82C9-0DCC085BDE07}.Release|Any CPU.Build.0 = Release|Any CPU {6EABF160-E21A-4ABD-82C9-0DCC085BDE07}.Release|x64.ActiveCfg = Release|x64 {6EABF160-E21A-4ABD-82C9-0DCC085BDE07}.Release|x64.Build.0 = Release|x64 + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Debug|x64.ActiveCfg = Debug|Any CPU + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Debug|x64.Build.0 = Debug|Any CPU + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Release|Any CPU.Build.0 = Release|Any CPU + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Release|x64.ActiveCfg = Release|Any CPU + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/QuickFIXn/Message/Message.cs b/QuickFIXn/Message/Message.cs index c3f7bd402..47b18527c 100644 --- a/QuickFIXn/Message/Message.cs +++ b/QuickFIXn/Message/Message.cs @@ -2,6 +2,7 @@ using System.Text; using QuickFix.Fields; using System.Text.RegularExpressions; +using System.Text.Json; using System.Collections.Generic; namespace QuickFix @@ -489,6 +490,72 @@ public void FromString(string msgstr, bool validate, } } + /// + /// Creates a Message from FIX JSON Encoding. + /// See: https://github.com/FIXTradingCommunity/fix-json-encoding-spec + /// + /// + /// + /// + /// + /// If null, any groups will be constructed as generic Group objects + public void FromJson(string json, bool validate, DataDictionary.DataDictionary sessionDD, DataDictionary.DataDictionary appDD, IMessageFactory msgFactory) + { + this.ApplicationDataDictionary = appDD; + Clear(); + + using (JsonDocument document = JsonDocument.Parse(json)) + { + string beginString = document.RootElement.GetProperty("Header").GetProperty("BeginString").GetString(); + string msgType = document.RootElement.GetProperty("Header").GetProperty("MsgType").GetString(); + DataDictionary.IFieldMapSpec msgMap = appDD.GetMapForMessage(msgType); + FromJson(document.RootElement.GetProperty("Header"), beginString, msgType, msgMap, msgFactory, sessionDD, this.Header); + FromJson(document.RootElement.GetProperty("Body"), beginString, msgType, msgMap, msgFactory, appDD, this); + FromJson(document.RootElement.GetProperty("Trailer"), beginString, msgType, msgMap, msgFactory, sessionDD, this.Trailer); + } + + this.Header.SetField(new BodyLength(BodyLength()), true); + this.Trailer.SetField(new CheckSum(Fields.Converters.CheckSumConverter.Convert(CheckSum())), true); + + if (validate) + { + Validate(); + } + } + + protected void FromJson(JsonElement jsonElement, string beginString, string msgType, DataDictionary.IFieldMapSpec msgMap, IMessageFactory msgFactory, DataDictionary.DataDictionary dataDict, FieldMap fieldMap) + { + foreach (JsonProperty field in jsonElement.EnumerateObject()) + { + DataDictionary.DDField ddField; + if (dataDict.FieldsByName.TryGetValue(field.Name.ToString(), out ddField)) + { + if ((null != msgMap) && (msgMap.IsGroup(ddField.Tag)) && (JsonValueKind.Array == field.Value.ValueKind)) + { + foreach (JsonElement jsonGrp in field.Value.EnumerateArray()) + { + Group grp = msgFactory.Create(beginString, msgType, ddField.Tag); + FromJson(jsonGrp, beginString, msgType, msgMap.GetGroupSpec(ddField.Tag), msgFactory, dataDict, grp); + fieldMap.AddGroup(grp); + } + } + + if (JsonValueKind.Array != field.Value.ValueKind) + { + fieldMap.SetField(new StringField(ddField.Tag, field.Value.ToString())); + } + } + else + { + // this may be a custom tag given by number instead of name + if (Int32.TryParse(field.Name.ToString(), out int customTagNumber)) + { + fieldMap.SetField(new StringField(customTagNumber, field.Value.ToString())); + } + } + } + } + /// /// Constructs a group and stores it in this Message object /// diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4c0a2441c..5db9d7be9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -21,6 +21,8 @@ it technically violates semantic versioning. change various Get/SetNextSenderMsgSeqNum & Get/SetNextTargetMsgSeqNum functions to properties (gbirchmeier) **Non-breaking changes** +* (minor) #745 - JSON-to-FIX (mgatny) +* (minor) #724 - FIX-to-JSON serialization, and a ToXML() bugfix (mgatny) * (patch) #647 - replace lock with memory barrier to avoid deadlocks (brunobelmondo) * (patch) #623 - fix issues with New-Release.ps1 (fourpastmidnight) * (minor) #732 - generate FIXT11 msg classes so they can be cracked (mgatny) diff --git a/UnitTests/MessageTests.cs b/UnitTests/MessageTests.cs index e135f1a1b..b7d4c4ae0 100644 --- a/UnitTests/MessageTests.cs +++ b/UnitTests/MessageTests.cs @@ -963,6 +963,95 @@ public void ChecksumIsLastFieldOfTrailer() string foo = msg.ToString().Replace(Message.SOH, "|"); StringAssert.EndsWith("|10=099|", foo); + } + + [Test] + [Category("JSON")] + public void JsonNestedRepeatingGroupParseGroupTest() + { + // Given the following string in FIX JSON Encoding: + string json = @" + { + ""Header"": { + ""BeginString"":""FIX.4.4"", + ""MsgSeqNum"":""360"", + ""MsgType"":""8"", + ""SenderCompID"":""BLPTSOX"", + ""SendingTime"":""20130321-15:21:23"", + ""TargetCompID"":""THINKTSOX"" + }, + ""Body"": { + ""31337"":""custom body field"", + ""AvgPx"":""122.255"", + ""ClOrdID"":""61101189"", + ""CumQty"":""1990000"", + ""ExecID"":""VCON:20130321:50018:5:12"", + ""LastPx"":""122.255"", + ""LastQty"":""1990000"", + ""OrderID"":""116"", + ""OrderQty"":""1990000"", + ""OrdStatus"":""2"", + ""Side"":""1"", + ""Symbol"":""[N/A]"", + ""TransactTime"":""20130321-15:21:23"", + ""ExecType"":""F"", + ""LeavesQty"":""0"", + ""NoPartyIDs"": [ + { + ""PartyIDSource"":""D"", + ""PartyID"":""OHAI"", + ""PartyRole"":""1"", + ""NoPartySubIDs"": [ + { + ""PartySubID"":""14"", + ""PartySubIDType"":""4"", + ""31338"":""custom group field"" + } + ] + }, + { ""PartyIDSource"":""D"", ""PartyID"":""TFOLIO:6804469"", ""PartyRole"":""12"" }, + { ""PartyIDSource"":""D"", ""PartyID"":""TFOLIO"", ""PartyRole"":""11"" }, + { ""PartyIDSource"":""D"", ""PartyID"":""THINKFOLIO LTD"", ""PartyRole"":""13"" }, + { ""PartyIDSource"":""D"", ""PartyID"":""SXT"", ""PartyRole"":""16"" }, + { ""PartyIDSource"":""D"", ""PartyID"":""TFOLIO:6804469"", ""PartyRole"":""36"" } + ] + }, + ""Trailer"": { + } + } + "; + + // When the JSON is parsed into a QuickFIX Message + var dd = new QuickFix.DataDictionary.DataDictionary(); + dd.LoadFIXSpec("FIX44"); + var msg = new Message(); + msg.FromJson(json, true, dd, dd, _defaultMsgFactory); + TestContext.Out.WriteLine(msg.ToString().Replace(Message.SOH, "|")); + + // Then the Header of the Message should contain: + Assert.That(msg.Header.GetString(Tags.BeginString), Is.EqualTo("FIX.4.4")); + Assert.That(msg.Header.GetString(Tags.MsgSeqNum), Is.EqualTo("360")); + Assert.That(msg.Header.GetString(Tags.BodyLength), Is.EqualTo("446")); + + // And the Body of the Message should contain: + Assert.That(msg.GetString(31337), Is.EqualTo("custom body field")); + Assert.That(msg.GetString(Tags.AvgPx), Is.EqualTo("122.255")); + Assert.That(msg.GetString(Tags.Symbol), Is.EqualTo("[N/A]")); + Assert.That(msg.GetString(Tags.OrdStatus), Is.EqualTo("2")); + Assert.That(msg.GetString(Tags.TransactTime), Is.EqualTo("20130321-15:21:23")); + + // And the NoPartyIDs Group should contain: + Assert.That(msg.GetString(Tags.NoPartyIDs), Is.EqualTo("6")); + + var noPartyGrp = msg.GetGroup(1, Tags.NoPartyIDs); + Assert.That(noPartyGrp.GetString(Tags.PartyID), Is.EqualTo("OHAI")); + Assert.That(noPartyGrp.GetString(Tags.PartyIDSource), Is.EqualTo("D")); + Assert.That(noPartyGrp.GetString(Tags.NoPartySubIDs), Is.EqualTo("1")); + + var noPartySubGrp = noPartyGrp.GetGroup(1, Tags.NoPartySubIDs); + Assert.That(noPartySubGrp.GetString(Tags.PartySubID), Is.EqualTo("14")); + Assert.That(noPartySubGrp.GetString(Tags.PartySubIDType), Is.EqualTo("4")); + Assert.That(noPartySubGrp.GetString(31338), Is.EqualTo("custom group field")); } } }