Skip to content

Commit

Permalink
Merge pull request #754 from gbirchmeier/jsontofix
Browse files Browse the repository at this point in the history
port @mgatny's "JSON to FIX" work to head
  • Loading branch information
gbirchmeier authored Jan 6, 2023
2 parents 05cb09e + 3f7f48a commit c4e8171
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 0 deletions.
16 changes: 16 additions & 0 deletions Examples/JsonToFix/Examples.JsonToFix.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\QuickFIXn\QuickFix.csproj" />
<ProjectReference Include="..\..\Messages\FIX50SP2\QuickFix.FIX50SP2.csproj" />
<ProjectReference Include="..\..\Messages\FIX44\QuickFix.FIX44.csproj" />
<ProjectReference Include="..\..\Messages\FIX42\QuickFix.FIX42.csproj" />
</ItemGroup>
</Project>
77 changes: 77 additions & 0 deletions Examples/JsonToFix/Program.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
10 changes: 10 additions & 0 deletions QuickFIXn.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions QuickFIXn/Message/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Text;
using QuickFix.Fields;
using System.Text.RegularExpressions;
using System.Text.Json;
using System.Collections.Generic;

namespace QuickFix
Expand Down Expand Up @@ -489,6 +490,72 @@ public void FromString(string msgstr, bool validate,
}
}

/// <summary>
/// Creates a Message from FIX JSON Encoding.
/// See: https://github.com/FIXTradingCommunity/fix-json-encoding-spec
/// </summary>
/// <param name="json"></param>
/// <param name="validate"></param>
/// <param name="sessionDD"></param>
/// <param name="appDD"></param>
/// <param name="msgFactory">If null, any groups will be constructed as generic Group objects</param>
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()));
}
}
}
}

/// <summary>
/// Constructs a group and stores it in this Message object
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
89 changes: 89 additions & 0 deletions UnitTests/MessageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
}
}

0 comments on commit c4e8171

Please sign in to comment.