ofx-rs
A Rust library for parsing Open Financial Exchange (OFX) documents into strongly-typed structures.
ofx-rs handles both OFX 1.x (SGML) and OFX 2.x (XML) formats through a single entry point, producing precise domain types that prevent common financial data bugs at compile time. The parser is pure -- no I/O, no unsafe code, no runtime surprises.
Quick Start
Add the dependency to your project:
Parse an OFX document with a single function call:
use parse;
// You provide the file content; the library does no I/O itself.
let ofx_content = read_to_string.unwrap;
let doc = parse.unwrap;
// Check signon status
assert!;
// Access the first bank statement
let banking = doc.banking.expect;
let stmt = banking.statement_responses
.response
.expect;
println!;
println!;
if let Some = stmt.ledger_balance
Supported Formats
The library parses both major OFX format families transparently:
- OFX 2.x (XML) -- Well-formed XML with a processing instruction header (
<?OFX ... ?>). Parsed directly by quick-xml. - OFX 1.x (SGML) -- Uses SGML where closing tags are optional. The library normalizes these documents to well-formed XML automatically before parsing, using knowledge of the OFX tag hierarchy to insert missing close tags.
Format detection is automatic based on the header. You always call parse() the same way regardless of version.
Examples
Iterating Over Transactions
use parse;
use TransactionType;
let doc = parse?;
let banking = doc.banking.expect;
let stmt = banking.statement_responses
.response
.expect;
// Account information
let acct = stmt.bank_account;
println!;
println!;
println!;
// Iterate and filter transactions
if let Some = stmt.transaction_list
Parsing a Credit Card Statement
use parse;
let doc = parse?;
if let Some = doc.credit_card
Handling OFX 1.x (SGML) Files
OFX 1.x files use SGML syntax where closing tags are optional:
OFXHEADER:100
DATA:OFXSGML
VERSION:102
SECURITY:NONE
ENCODING:USASCII
CHARSET:1252
COMPRESSION:NONE
OLDFILEUID:NONE
NEWFILEUID:NONE
<OFX>
<SIGNONMSGSRSV1>
<SONRS>
<STATUS>
<CODE>0
<SEVERITY>INFO
<DTSERVER>20230115
<LANGUAGE>ENG
The library normalizes this automatically. The API is identical regardless of format version:
use parse;
// Works the same for both OFX 1.x and 2.x
let doc = parse?;
println!;
API Overview
parse() returns an OfxDocument that mirrors the OFX document tree:
OfxDocument
|-- header: OfxHeader (version, security level, file UIDs)
|-- signon: SignonResponse (status, server datetime, language, FI info)
|-- banking: BankingMessageSet (optional)
| |-- statement_responses: Vec<TransactionWrapper<StatementResponse>>
| |-- status: Status
| |-- response: StatementResponse
| |-- currency_default: CurrencyCode
| |-- bank_account: BankAccount
| |-- transaction_list: TransactionList (optional)
| | |-- transactions: Vec<StatementTransaction>
| |-- ledger_balance: LedgerBalance (optional)
| |-- available_balance: AvailableBalance (optional)
| |-- balance_list: Vec<Balance>
|-- credit_card: CreditCardMessageSet (optional)
|-- statement_responses: Vec<TransactionWrapper<CcStatementResponse>>
|-- (same structure as banking, with CreditCardAccount)
Every field is accessed through methods on the returned structs. Optional fields return Option<&T>, and collections return slices.
Type System
Financial data demands precision. ofx-rs uses domain-specific types rather than raw strings and floats:
| Type | Wraps | Purpose |
|---|---|---|
OfxAmount |
rust_decimal::Decimal |
Exact financial arithmetic -- no floating-point rounding |
OfxDateTime |
time::OffsetDateTime |
OFX datetime format with timezone support |
CurrencyCode |
Validated String |
ISO 4217 currency codes (USD, EUR, BRL) |
TransactionType |
Enum (18 variants) | CREDIT, DEBIT, CHECK, ATM, POS, XFER, and more |
AccountType |
Enum | CHECKING, SAVINGS, MONEYMRKT, CREDITLINE |
BankId, AccountId, FitId |
Validated newtypes | Length-validated identifiers that reject empty strings |
CheckNumber |
Validated newtype | Check number with spec-compliant length constraints |
OfxAmount supports arithmetic operations (Add, Sub, Neg) and convenience methods like is_negative() and is_zero():
let amount: OfxAmount = "-50.00".parse.unwrap;
assert!;
// Decimal precision preserved
assert_eq!;
OfxDateTime parses the OFX datetime format with right-truncation and timezone offsets:
// Date only
let dt: OfxDateTime = "20230115".parse.unwrap;
// Full datetime with timezone
let dt: OfxDateTime = "20230115120000[-5:EST]".parse.unwrap;
Error Handling
All errors are structured and non-panicking. The top-level OfxError enum distinguishes three failure categories:
OfxError::Header-- The OFX header is missing, malformed, or contains an unrecognized version or security level.OfxError::Xml-- The XML body is malformed, a required element is missing, or an element contains invalid content.OfxError::Aggregate-- A required field within an OFX aggregate (like a transaction missing its FITID) is absent.
OfxError and its inner types are marked #[non_exhaustive], so match statements require a wildcard arm. Each variant provides specific context about what went wrong and where:
use ;
match parse
Dependencies
ofx-rs depends on three crates, chosen for correctness over convenience:
- quick-xml -- Fast, zero-copy XML parsing
- rust_decimal -- Exact decimal arithmetic for financial amounts
- time -- Date and time handling with timezone support
No runtime, no async, no macros, no build scripts.
Minimum Supported Rust Version
This crate uses edition = "2024" and requires Rust 1.85 or later.
License
MIT