JSON to FIX Message Conversion with miniFIX
Introduction
Modern financial systems often need to work with multiple message formats. While FIX is the standard for electronic trading, many systems also use JSON for APIs, configuration, testing, and integration with web-based interfaces. miniFIX provides seamless conversion between JSON and FIX tag=value formats, enabling flexible system architectures.
What You’ll Learn
- Converting JSON messages to FIX tag=value format
- Understanding miniFIX’s dual-format support
- Working with JSON field mapping to FIX tags
- Building conversion pipelines for integration scenarios
- Best practices for format conversion in financial systems
The Business Case
Consider these common scenarios where JSON-to-FIX conversion is valuable:
- API Integration: Web APIs sending orders that need to be converted to FIX
- Testing: Using human-readable JSON for test cases, converting to FIX for execution
- Configuration: Storing FIX message templates in JSON format
- Logging: Converting FIX messages to JSON for analysis and storage
- Cross-System Integration: Bridge between JSON-based systems and FIX-based trading venues
JSON FIX Message Structure
miniFIX uses a structured JSON format that mirrors FIX message organization:
{
"Header": {
"BeginString": "FIX.4.4",
"MsgType": "W",
"MsgSeqNum": "4567",
"SenderCompID": "SENDER",
"TargetCompID": "TARGET",
"SendingTime": "20160802-21:14:38.717"
},
"Body": {
"SecurityIDSource": "8",
"SecurityID": "ESU6",
"MDReqID": "789",
"NoMDEntries": [
{
"MDEntryType": "0",
"MDEntryPx": "1.50",
"MDEntrySize": "75",
"MDEntryTime": "21:14:38.688"
},
{
"MDEntryType": "1",
"MDEntryPx": "1.75",
"MDEntrySize": "25",
"MDEntryTime": "21:14:38.688"
}
]
},
"Trailer": {
}
}
Basic JSON to FIX Conversion
Let’s start with a simple conversion example:
use minifix::json::FieldOrGroup;
use minifix::prelude::*;
const JSON_FIX_MESSAGE: &str = r#"
{
"Header": {
"BeginString": "FIX.4.2",
"MsgType": "D",
"MsgSeqNum": "123",
"SenderCompID": "CLIENT",
"TargetCompID": "BROKER",
"SendingTime": "20241201-10:30:15"
},
"Body": {
"ClOrdID": "ORDER_001",
"Symbol": "AAPL",
"Side": "1",
"OrderQty": "100",
"OrdType": "2",
"Price": "150.25"
},
"Trailer": {}
}
"#;
fn convert_json_to_fix() -> Result<String, Box<dyn std::error::Error>> {
// Create decoders and encoders
let dictionary = minifix::Dictionary::fix42();
let mut json_decoder = minifix::json::Decoder::new(dictionary.clone());
let mut fix_encoder = minifix::tagvalue::Encoder::new();
let mut buffer = Vec::new();
// Decode JSON message
let json_msg = json_decoder.decode(JSON_FIX_MESSAGE.as_bytes())?;
// Extract essential fields for FIX message creation
let msg_type = json_msg.get(fix42::MSG_TYPE)?;
let begin_string = json_msg.get(fix42::BEGIN_STRING)?;
// Start building FIX message
let mut fix_msg_builder = fix_encoder.start_message(begin_string, &mut buffer, msg_type);
// Convert all fields from JSON to FIX
for (field_name, field_value) in json_msg.iter_fields() {
let field = dictionary
.field_by_name(field_name)
.ok_or_else(|| format!("Unknown field: {}", field_name))?;
match field_value {
FieldOrGroup::Field(value) => {
fix_msg_builder.set(field.tag(), value.as_ref());
}
FieldOrGroup::Group(_group) => {
// TODO: Handle repeating groups
println!("β οΈ Repeating group conversion not implemented: {}", field_name);
}
}
}
// Finalize the FIX message
let (fix_message, _) = fix_msg_builder.done();
Ok(String::from_utf8_lossy(fix_message).to_string())
}
fn main() {
match convert_json_to_fix() {
Ok(fix_message) => {
println!("β
JSON to FIX conversion successful!");
println!("π€ FIX Message:");
println!("{}", fix_message);
}
Err(e) => {
eprintln!("β Conversion failed: {}", e);
}
}
}
Advanced Conversion with Field Validation
Let’s build a more robust converter with proper validation and error handling:
use minifix::json::FieldOrGroup;
use minifix::prelude::*;
use serde_json::Value;
use std::collections::HashMap;
pub struct JsonToFixConverter {
dictionary: Dictionary,
json_decoder: minifix::json::Decoder,
fix_encoder: minifix::tagvalue::Encoder,
}
impl JsonToFixConverter {
pub fn new(fix_version: FixVersion) -> Self {
let dictionary = match fix_version {
FixVersion::Fix42 => Dictionary::fix42(),
FixVersion::Fix44 => Dictionary::fix44(),
};
let json_decoder = minifix::json::Decoder::new(dictionary.clone());
let fix_encoder = minifix::tagvalue::Encoder::new();
Self {
dictionary,
json_decoder,
fix_encoder,
}
}
pub fn convert(&mut self, json_input: &str) -> Result<Vec<u8>, ConversionError> {
// Parse and validate JSON structure
let json_value: Value = serde_json::from_str(json_input)
.map_err(ConversionError::JsonParse)?;
self.validate_json_structure(&json_value)?;
// Decode with miniFIX JSON decoder
let json_msg = self.json_decoder.decode(json_input.as_bytes())
.map_err(ConversionError::JsonDecode)?;
// Convert to FIX format
let mut buffer = Vec::new();
self.convert_to_fix(&json_msg, &mut buffer)?;
Ok(buffer)
}
fn validate_json_structure(&self, json: &Value) -> Result<(), ConversionError> {
let obj = json.as_object()
.ok_or(ConversionError::InvalidStructure("Root must be object"))?;
// Validate required sections
if !obj.contains_key("Header") {
return Err(ConversionError::MissingSection("Header"));
}
if !obj.contains_key("Body") {
return Err(ConversionError::MissingSection("Body"));
}
// Validate header has required fields
let header = obj["Header"].as_object()
.ok_or(ConversionError::InvalidStructure("Header must be object"))?;
for required_field in ["BeginString", "MsgType"] {
if !header.contains_key(required_field) {
return Err(ConversionError::MissingField(required_field.to_string()));
}
}
Ok(())
}
fn convert_to_fix(
&mut self,
json_msg: &impl GetField<u32>,
buffer: &mut Vec<u8>
) -> Result<(), ConversionError> {
// Extract message metadata
let msg_type = json_msg.get(fix42::MSG_TYPE)
.map_err(|_| ConversionError::MissingField("MsgType".to_string()))?;
let begin_string = json_msg.get(fix42::BEGIN_STRING)
.map_err(|_| ConversionError::MissingField("BeginString".to_string()))?;
// Start FIX message
let mut fix_builder = self.fix_encoder.start_message(begin_string, buffer, msg_type);
// Convert all fields
for (field_name, field_value) in json_msg.iter_fields() {
if let Some(field_def) = self.dictionary.field_by_name(field_name) {
match field_value {
FieldOrGroup::Field(value) => {
self.convert_field(&mut fix_builder, field_def, value.as_ref())?;
}
FieldOrGroup::Group(group) => {
self.convert_group(&mut fix_builder, field_def, group)?;
}
}
} else {
// Log warning for unknown fields but continue
eprintln!("β οΈ Unknown field ignored: {}", field_name);
}
}
fix_builder.done();
Ok(())
}
fn convert_field(
&self,
fix_builder: &mut impl SetField<u32>,
field_def: &impl FieldDefinition,
value: &[u8],
) -> Result<(), ConversionError> {
// Validate field value based on type
let field_type = field_def.field_type();
match field_type {
FieldType::Int => {
let _: i32 = std::str::from_utf8(value)
.map_err(|_| ConversionError::InvalidFieldValue(field_def.tag(), "not UTF-8"))?
.parse()
.map_err(|_| ConversionError::InvalidFieldValue(field_def.tag(), "not integer"))?;
}
FieldType::Float => {
let _: f64 = std::str::from_utf8(value)
.map_err(|_| ConversionError::InvalidFieldValue(field_def.tag(), "not UTF-8"))?
.parse()
.map_err(|_| ConversionError::InvalidFieldValue(field_def.tag(), "not float"))?;
}
_ => {
// For other types, basic UTF-8 validation
std::str::from_utf8(value)
.map_err(|_| ConversionError::InvalidFieldValue(field_def.tag(), "not UTF-8"))?;
}
}
fix_builder.set(field_def.tag(), value);
Ok(())
}
fn convert_group(
&self,
_fix_builder: &mut impl SetField<u32>,
_field_def: &impl FieldDefinition,
_group: &minifix::json::Group,
) -> Result<(), ConversionError> {
// TODO: Implement repeating group conversion
eprintln!("β οΈ Repeating group conversion not yet implemented");
Ok(())
}
}
#[derive(Debug)]
pub enum FixVersion {
Fix42,
Fix44,
}
#[derive(Debug, thiserror::Error)]
pub enum ConversionError {
#[error("JSON parse error: {0}")]
JsonParse(#[from] serde_json::Error),
#[error("JSON decode error: {0}")]
JsonDecode(String),
#[error("Invalid JSON structure: {0}")]
InvalidStructure(&'static str),
#[error("Missing section: {0}")]
MissingSection(&'static str),
#[error("Missing field: {0}")]
MissingField(String),
#[error("Invalid field value for tag {0}: {1}")]
InvalidFieldValue(u32, &'static str),
}
Bidirectional Conversion: FIX to JSON
miniFIX also supports converting FIX messages to JSON format:
pub struct FixToJsonConverter {
dictionary: Dictionary,
fix_decoder: minifix::tagvalue::Decoder,
}
impl FixToJsonConverter {
pub fn new(fix_version: FixVersion) -> Self {
let dictionary = match fix_version {
FixVersion::Fix42 => Dictionary::fix42(),
FixVersion::Fix44 => Dictionary::fix44(),
};
let fix_decoder = minifix::tagvalue::Decoder::new(dictionary.clone());
Self {
dictionary,
fix_decoder,
}
}
pub fn convert(&mut self, fix_message: &[u8]) -> Result<String, ConversionError> {
// Decode FIX message
let msg = self.fix_decoder.decode(fix_message)
.map_err(|e| ConversionError::JsonDecode(format!("FIX decode error: {:?}", e)))?;
// Build JSON structure
let mut json_obj = serde_json::Map::new();
// Create header, body, and trailer sections
let mut header = serde_json::Map::new();
let mut body = serde_json::Map::new();
let trailer = serde_json::Map::new();
// Categorize fields
for (field_name, field_value) in msg.iter_fields() {
let field_def = self.dictionary.field_by_name(field_name)
.ok_or_else(|| ConversionError::MissingField(field_name.to_string()))?;
let tag = field_def.tag();
let value = match field_value {
FieldOrGroup::Field(data) => {
serde_json::Value::String(String::from_utf8_lossy(data.as_ref()).to_string())
}
FieldOrGroup::Group(_) => {
// TODO: Convert repeating groups to JSON arrays
serde_json::Value::String("GROUP_NOT_IMPLEMENTED".to_string())
}
};
// Categorize by tag ranges (simplified)
if tag <= 56 {
header.insert(field_name.to_string(), value);
} else if tag == 10 {
// Checksum goes in trailer (though it's usually empty in JSON)
} else {
body.insert(field_name.to_string(), value);
}
}
json_obj.insert("Header".to_string(), serde_json::Value::Object(header));
json_obj.insert("Body".to_string(), serde_json::Value::Object(body));
json_obj.insert("Trailer".to_string(), serde_json::Value::Object(trailer));
let json_value = serde_json::Value::Object(json_obj);
serde_json::to_string_pretty(&json_value)
.map_err(ConversionError::JsonParse)
}
}
Complete Conversion Pipeline
Here’s a complete example showing bidirectional conversion:
use minifix::json::FieldOrGroup;
use minifix::prelude::*;
const SAMPLE_JSON: &str = include_str!("fix-example.json");
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("π miniFIX JSON β FIX Conversion Demo");
println!("====================================");
// Step 1: JSON to FIX conversion
println!("π Original JSON Message:");
println!("{}", SAMPLE_JSON);
let dictionary = Dictionary::fix42();
let mut json_decoder = minifix::json::Decoder::new(dictionary.clone());
let mut fix_encoder = minifix::tagvalue::Encoder::new();
let mut buffer = Vec::new();
let json_msg = json_decoder.decode(SAMPLE_JSON.as_bytes())?;
let msg_type = json_msg.get(fix42::MSG_TYPE)?;
let begin_string = json_msg.get(fix42::BEGIN_STRING)?;
let mut fix_msg_builder = fix_encoder.start_message(begin_string, &mut buffer, msg_type);
println!("\nπ§ Converting JSON fields to FIX:");
for (field_name, field_value) in json_msg.iter_fields() {
if let Some(field) = dictionary.field_by_name(field_name) {
match field_value {
FieldOrGroup::Field(s) => {
println!(" {} (tag {}): {}",
field_name,
field.tag(),
String::from_utf8_lossy(s.as_ref())
);
fix_msg_builder.set(field.tag(), s.as_ref());
}
FieldOrGroup::Group(_g) => {
println!(" {} (tag {}): [GROUP]", field_name, field.tag());
// Groups would be handled here
}
}
}
}
let (fix_message, _) = fix_msg_builder.done();
println!("\nπ€ Generated FIX Message:");
println!("{}", String::from_utf8_lossy(fix_message));
// Step 2: Verify by parsing the FIX message back
println!("\nβ
Verification - Parsing generated FIX:");
let mut fix_decoder = minifix::tagvalue::Decoder::new(dictionary);
let parsed_msg = fix_decoder.decode(fix_message)?;
println!(" Message Type: {:?}",
String::from_utf8_lossy(parsed_msg.get(fix42::MSG_TYPE)?));
println!(" Sender: {:?}",
String::from_utf8_lossy(parsed_msg.get(fix42::SENDER_COMP_ID)?));
println!(" Symbol: {:?}",
String::from_utf8_lossy(parsed_msg.get(fix42::SYMBOL).unwrap_or(b"N/A")));
println!("\nπ Conversion completed successfully!");
Ok(())
}
Production Use Cases
API Gateway
use axum::{extract::Json, http::StatusCode, response::Json as JsonResponse};
pub async fn submit_order(Json(order_json): Json<serde_json::Value>) -> Result<JsonResponse<serde_json::Value>, StatusCode> {
let mut converter = JsonToFixConverter::new(FixVersion::Fix44);
match converter.convert(&order_json.to_string()) {
Ok(fix_message) => {
// Send to trading venue
send_to_venue(&fix_message).await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(JsonResponse(serde_json::json!({
"status": "accepted",
"message": "Order submitted successfully"
})))
}
Err(e) => {
eprintln!("Conversion error: {:?}", e);
Err(StatusCode::BAD_REQUEST)
}
}
}
Message Logging
pub struct MessageLogger {
fix_to_json: FixToJsonConverter,
}
impl MessageLogger {
pub fn log_fix_message(&mut self, fix_message: &[u8], direction: Direction) -> Result<(), LogError> {
match self.fix_to_json.convert(fix_message) {
Ok(json_str) => {
let log_entry = LogEntry {
timestamp: chrono::Utc::now(),
direction,
format: "JSON",
content: json_str,
};
// Write to log storage
self.write_log_entry(&log_entry)
}
Err(e) => {
// Fallback to raw FIX logging
let log_entry = LogEntry {
timestamp: chrono::Utc::now(),
direction,
format: "FIX_RAW",
content: String::from_utf8_lossy(fix_message).to_string(),
};
self.write_log_entry(&log_entry)
}
}
}
}
Best Practices
- Validation: Always validate JSON structure and field values
- Error Handling: Provide meaningful error messages for conversion failures
- Performance: Reuse converters to avoid repeated dictionary loading
- Logging: Log conversion errors and warnings for debugging
- Testing: Test with representative sample data from your environment
- Field Mapping: Maintain clear documentation of JSON-to-FIX field mappings
Limitations and Considerations
- Repeating Groups: Complex group structures need special handling
- Precision: Be careful with numeric precision in JSON numbers
- Time Zones: Handle timestamp conversions carefully
- Custom Fields: Ensure custom fields are defined in your dictionary
- Performance: JSON parsing adds overhead compared to native FIX
The JSON-to-FIX conversion capabilities in miniFIX provide powerful integration options for modern financial systems, enabling seamless communication between JSON-based APIs and FIX-based trading infrastructure.