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"));
}
}
}