πŸ¦€ miniFIX Tutorial | Part 05

September 19, 2025 by CryptoPatrick Rust Tutorials

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

  1. Validation: Always validate JSON structure and field values
  2. Error Handling: Provide meaningful error messages for conversion failures
  3. Performance: Reuse converters to avoid repeated dictionary loading
  4. Logging: Log conversion errors and warnings for debugging
  5. Testing: Test with representative sample data from your environment
  6. 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.


Next: Part 06 - Field Validation

'I write to understand as much as to be understood.' β€”Elie Wiesel
(c) 2024 CryptoPatrick