swift_mt_message/
lib.rs

1//! # Swift MT Message Parser - Enhanced Architecture
2//!
3//! A comprehensive Rust library for parsing SWIFT MT (Message Type) messages with strong typing,
4//! complex field structures, comprehensive validation, and flattened JSON serialization.
5//!
6//! ## Key Features
7//!
8//! - **Complex Field Structures**: Full enum-based field variants (Field50: A/F/K, Field59: A/Basic)
9//! - **Flattened JSON Serialization**: Clean JSON output without enum wrapper layers
10//! - **Type-safe field parsing** with dedicated field structs and automatic validation
11//! - **Comprehensive Field Support**: All MT103 fields with proper SWIFT compliance
12//! - **Bidirectional Serialization**: Perfect round-trip JSON serialization/deserialization
13//! - **Extensive Validation**: BIC validation, field length checks, format compliance
14//!
15//! ## Supported Field Types
16//!
17//! ### Complex Enum Fields
18//! - **Field50** (Ordering Customer): 50A (Account+BIC), 50F (Party+Address), 50K (Name+Address)
19//! - **Field59** (Beneficiary Customer): 59A (Account+BIC), 59 (Basic lines)
20//!
21//! ### Institution Fields (with account_line_indicator)
22//! - **Field52A** (Ordering Institution): BIC + optional account + account_line_indicator
23//! - **Field53A-57A** (Correspondent/Intermediary): All with account_line_indicator support
24//!
25//! ### Simple Type Fields
26//! - **Field32A** (Value Date/Currency/Amount): NaiveDate + String + f64
27//! - **Field20, 23B, 70, 71A**: Proper field name alignment with old version
28//!
29//! ## Example Usage
30//!
31//! ```rust
32//! use swift_mt_message::{SwiftParser, SwiftMessage, messages::MT103};
33//!
34//! let raw_mt103 = "{1:F01BANKDEFFAXXX0123456789}{2:I103BANKDEFFAXXXU3003}{4:\n:20:FT21234567890\n:23B:CRED\n:32A:210315EUR1234567,89\n:50K:JOHN DOE\n:52A:BANKDEFF\n:57A:DEUTDEFF\n:59A:/DE89370400440532013000\nDEUTDEFF\n:70:PAYMENT\n:71A:OUR\n-}";
35//! let parsed: SwiftMessage<MT103> = SwiftParser::parse(raw_mt103)?;
36//! let json_output = serde_json::to_string_pretty(&parsed)?;
37//! # Ok::<(), Box<dyn std::error::Error>>(())
38//! ```
39//!
40//! ## JSON Output Structure
41//!
42//! The library produces clean, flattened JSON without enum wrapper layers:
43//!
44//! ```json
45//! {
46//!   "50": {
47//!     "name_and_address": ["JOHN DOE", "123 MAIN ST"]
48//!   },
49//!   "59": {
50//!     "account": "DE89370400440532013000",
51//!     "bic": "DEUTDEFFXXX"
52//!   }
53//! }
54//! ```
55//!
56//! Instead of nested enum structures like `{"50": {"K": {...}}}`.
57
58use serde::{Deserialize, Serialize};
59use std::collections::HashMap;
60use std::fmt::Debug;
61
62pub mod errors;
63pub mod fields;
64pub mod headers;
65pub mod messages;
66pub mod parser;
67
68// Re-export core types
69pub use errors::{ParseError, Result, ValidationError};
70pub use headers::{ApplicationHeader, BasicHeader, Trailer, UserHeader};
71pub use parser::SwiftParser;
72
73// Re-export derive macros
74pub use swift_mt_message_macros::{SwiftField, SwiftMessage, swift_serde};
75
76/// Simplified result type for SWIFT operations
77pub type SwiftResult<T> = std::result::Result<T, crate::errors::ParseError>;
78
79/// Core trait for all Swift field types
80pub trait SwiftField: Serialize + for<'de> Deserialize<'de> + Clone + std::fmt::Debug {
81    /// Parse field value from string representation
82    fn parse(value: &str) -> Result<Self>
83    where
84        Self: Sized;
85
86    /// Convert field back to SWIFT string format
87    fn to_swift_string(&self) -> String;
88
89    /// Validate field according to SWIFT format rules
90    fn validate(&self) -> ValidationResult;
91
92    /// Get field format specification
93    fn format_spec() -> &'static str;
94}
95
96/// Core trait for Swift message types
97pub trait SwiftMessageBody: Debug + Clone + Send + Sync + Serialize {
98    /// Get the message type identifier (e.g., "103", "202")
99    fn message_type() -> &'static str;
100
101    /// Create from field map
102    fn from_fields(fields: HashMap<String, Vec<String>>) -> SwiftResult<Self>
103    where
104        Self: Sized;
105
106    /// Convert to field map
107    fn to_fields(&self) -> HashMap<String, Vec<String>>;
108
109    /// Get required field tags for this message type
110    fn required_fields() -> Vec<&'static str>;
111
112    /// Get optional field tags for this message type
113    fn optional_fields() -> Vec<&'static str>;
114}
115
116/// Complete SWIFT message with headers and body
117#[derive(Debug, Clone, Serialize)]
118pub struct SwiftMessage<T: SwiftMessageBody> {
119    /// Basic Header (Block 1)
120    pub basic_header: BasicHeader,
121
122    /// Application Header (Block 2)
123    pub application_header: ApplicationHeader,
124
125    /// User Header (Block 3) - Optional
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub user_header: Option<UserHeader>,
128
129    /// Trailer (Block 5) - Optional
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub trailer: Option<Trailer>,
132
133    /// Raw message blocks for preservation
134    pub blocks: RawBlocks,
135
136    /// Message type identifier
137    pub message_type: String,
138
139    /// Field order as they appeared in the original message
140    pub field_order: Vec<String>,
141
142    /// Parsed message body with typed fields
143    pub fields: T,
144}
145
146/// Raw message blocks for preservation and reconstruction
147#[derive(Debug, Clone, Serialize, Deserialize, Default)]
148pub struct RawBlocks {
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub block1: Option<String>,
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub block2: Option<String>,
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub block3: Option<String>,
155    pub block4: String,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub block5: Option<String>,
158}
159
160/// Validation result for field and message validation
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct ValidationResult {
163    pub is_valid: bool,
164    pub errors: Vec<ValidationError>,
165    pub warnings: Vec<String>,
166}
167
168impl ValidationResult {
169    pub fn valid() -> Self {
170        Self {
171            is_valid: true,
172            errors: Vec::new(),
173            warnings: Vec::new(),
174        }
175    }
176
177    pub fn with_error(error: ValidationError) -> Self {
178        Self {
179            is_valid: false,
180            errors: vec![error],
181            warnings: Vec::new(),
182        }
183    }
184
185    pub fn with_errors(errors: Vec<ValidationError>) -> Self {
186        Self {
187            is_valid: errors.is_empty(),
188            errors,
189            warnings: Vec::new(),
190        }
191    }
192}
193
194/// Common data types used across multiple fields
195pub mod common {
196    use crate::ValidationResult;
197    use crate::errors::{ParseError, ValidationError};
198    use serde::{Deserialize, Serialize};
199
200    /// SWIFT BIC (Bank Identifier Code) with comprehensive validation and utilities
201    ///
202    /// A Bank Identifier Code (BIC) is used to identify financial institutions in SWIFT messages.
203    /// It consists of either 8 or 11 characters:
204    /// - Bank Code (4 characters): Alphabetic
205    /// - Country Code (2 characters): Alphabetic (ISO 3166-1 alpha-2)
206    /// - Location Code (2 characters): Alphanumeric
207    /// - Branch Code (3 characters, optional): Alphanumeric
208    ///
209    /// # Examples
210    /// - `CHASUS33XXX` - Chase Bank, US, New York, Branch: XXX
211    /// - `DEUTDEFF` - Deutsche Bank, Germany, Frankfurt (8-character format)
212    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
213    pub struct BIC {
214        /// The full BIC code (8 or 11 characters)
215        #[serde(rename = "bic")]
216        pub value: String,
217    }
218
219    impl BIC {
220        /// Create a new BIC with validation
221        ///
222        /// # Arguments
223        /// * `value` - The BIC string to validate and store
224        ///
225        /// # Returns
226        /// * `Result<BIC, ParseError>` - The validated BIC or an error
227        ///
228        /// # Examples
229        /// ```
230        /// use swift_mt_message::common::BIC;
231        /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
232        /// let bic = BIC::new("CHASUS33XXX".to_string())?;
233        /// assert_eq!(bic.bank_code(), "CHAS");
234        /// # Ok(())
235        /// # }
236        /// ```
237        pub fn new(value: impl Into<String>) -> Result<Self, ParseError> {
238            let value = value.into().to_uppercase();
239            let bic = Self { value };
240            bic.validate_strict()?;
241            Ok(bic)
242        }
243
244        /// Create a BIC without validation (for internal use)
245        pub fn new_unchecked(value: impl Into<String>) -> Self {
246            Self {
247                value: value.into().to_uppercase(),
248            }
249        }
250
251        /// Parse a BIC from a string with optional field tag context
252        pub fn parse(value: &str, field_tag: Option<&str>) -> Result<Self, ParseError> {
253            let value = value.trim().to_uppercase();
254
255            if value.is_empty() {
256                return Err(ParseError::InvalidFieldFormat {
257                    field_tag: field_tag.unwrap_or("BIC").to_string(),
258                    message: "BIC cannot be empty".to_string(),
259                });
260            }
261
262            let bic = Self::new_unchecked(value);
263            bic.validate_with_context(field_tag)?;
264            Ok(bic)
265        }
266
267        /// Strict validation that returns ParseError for use in constructors
268        fn validate_strict(&self) -> Result<(), ParseError> {
269            self.validate_with_context(None)
270        }
271
272        /// Validation with field context for better error messages
273        fn validate_with_context(&self, field_tag: Option<&str>) -> Result<(), ParseError> {
274            let field_name = field_tag.unwrap_or("BIC");
275
276            if self.value.len() != 8 && self.value.len() != 11 {
277                return Err(ParseError::InvalidFieldFormat {
278                    field_tag: field_name.to_string(),
279                    message: "BIC must be 8 or 11 characters".to_string(),
280                });
281            }
282
283            let bank_code = &self.value[0..4];
284            let country_code = &self.value[4..6];
285            let location_code = &self.value[6..8];
286
287            // Validate bank code (4 alphabetic characters)
288            if !bank_code.chars().all(|c| c.is_ascii_alphabetic()) {
289                return Err(ParseError::InvalidFieldFormat {
290                    field_tag: field_name.to_string(),
291                    message: "BIC bank code (first 4 characters) must be alphabetic".to_string(),
292                });
293            }
294
295            // Validate country code (2 alphabetic characters)
296            if !country_code.chars().all(|c| c.is_ascii_alphabetic()) {
297                return Err(ParseError::InvalidFieldFormat {
298                    field_tag: field_name.to_string(),
299                    message: "BIC country code (characters 5-6) must be alphabetic".to_string(),
300                });
301            }
302
303            // Validate location code (2 alphanumeric characters)
304            if !location_code.chars().all(|c| c.is_ascii_alphanumeric()) {
305                return Err(ParseError::InvalidFieldFormat {
306                    field_tag: field_name.to_string(),
307                    message: "BIC location code (characters 7-8) must be alphanumeric".to_string(),
308                });
309            }
310
311            // Validate branch code if present (3 alphanumeric characters)
312            if self.value.len() == 11 {
313                let branch_code = &self.value[8..11];
314                if !branch_code.chars().all(|c| c.is_ascii_alphanumeric()) {
315                    return Err(ParseError::InvalidFieldFormat {
316                        field_tag: field_name.to_string(),
317                        message: "BIC branch code (characters 9-11) must be alphanumeric"
318                            .to_string(),
319                    });
320                }
321            }
322
323            Ok(())
324        }
325
326        /// Validate BIC and return ValidationResult for field validation
327        pub fn validate(&self) -> ValidationResult {
328            match self.validate_strict() {
329                Ok(()) => ValidationResult::valid(),
330                Err(ParseError::InvalidFieldFormat { message, .. }) => {
331                    ValidationResult::with_error(ValidationError::FormatValidation {
332                        field_tag: "BIC".to_string(),
333                        message,
334                    })
335                }
336                Err(e) => ValidationResult::with_error(ValidationError::FormatValidation {
337                    field_tag: "BIC".to_string(),
338                    message: e.to_string(),
339                }),
340            }
341        }
342
343        /// Get the full BIC value
344        pub fn value(&self) -> &str {
345            &self.value
346        }
347
348        /// Get the bank code (first 4 characters)
349        pub fn bank_code(&self) -> &str {
350            &self.value[0..4]
351        }
352
353        /// Get the country code (characters 5-6)
354        pub fn country_code(&self) -> &str {
355            &self.value[4..6]
356        }
357
358        /// Get the location code (characters 7-8)
359        pub fn location_code(&self) -> &str {
360            &self.value[6..8]
361        }
362
363        /// Get the branch code if present (characters 9-11)
364        pub fn branch_code(&self) -> Option<&str> {
365            if self.value.len() == 11 {
366                Some(&self.value[8..11])
367            } else {
368                None
369            }
370        }
371
372        /// Check if this is a full BIC (11 characters) vs short BIC (8 characters)
373        pub fn is_full_bic(&self) -> bool {
374            self.value.len() == 11
375        }
376
377        /// Check if this institution is in a major financial center
378        pub fn is_major_financial_center(&self) -> bool {
379            let country = self.country_code();
380            let location = self.location_code();
381
382            matches!(
383                (country, location),
384                ("US", "33") | // New York
385                ("GB", "22") | // London
386                ("DE", "FF") | // Frankfurt
387                ("JP", "22") | // Tokyo
388                ("HK", "HK") | // Hong Kong
389                ("SG", "SG") | // Singapore
390                ("FR", "PP") | // Paris
391                ("CH", "ZZ") | // Zurich
392                ("CA", "TT") | // Toronto
393                ("AU", "MM") // Melbourne/Sydney
394            )
395        }
396
397        /// Check if this is a retail banking institution (heuristic based on common bank codes)
398        pub fn is_retail_bank(&self) -> bool {
399            let bank_code = self.bank_code();
400
401            // Common retail bank codes (this is a simplified check)
402            matches!(
403                bank_code,
404                "CHAS" | // Chase
405                "BOFA" | // Bank of America
406                "WELL" | // Wells Fargo
407                "CITI" | // Citibank
408                "HSBC" | // HSBC
409                "BARC" | // Barclays
410                "LLOY" | // Lloyds
411                "NATS" | // NatWest
412                "DEUT" | // Deutsche Bank
413                "COMM" | // Commerzbank
414                "BNPA" | // BNP Paribas
415                "CRED" | // Credit Agricole
416                "UBSW" | // UBS
417                "CRSU" | // Credit Suisse
418                "ROYA" | // Royal Bank of Canada
419                "TDOM" | // TD Bank
420                "ANZI" | // ANZ
421                "CTBA" | // Commonwealth Bank
422                "WEST" | // Westpac
423                "MUFG" | // MUFG Bank
424                "SMBC" | // Sumitomo Mitsui
425                "MIZB" // Mizuho Bank
426            )
427        }
428
429        /// Check if this institution's country supports real-time payments
430        pub fn supports_real_time_payments(&self) -> bool {
431            let country = self.country_code();
432
433            // Countries with major real-time payment systems
434            matches!(
435                country,
436                "US" | // FedNow, RTP
437                "GB" | // Faster Payments
438                "DE" | // Instant Payments
439                "NL" | // iDEAL
440                "SE" | // Swish
441                "DK" | // MobilePay
442                "AU" | // NPP
443                "SG" | // FAST
444                "IN" | // UPI
445                "BR" | // PIX
446                "MX" | // SPEI
447                "JP" | // Zengin
448                "KR" | // KFTC
449                "CN" // CIPS
450            )
451        }
452
453        /// Get the regulatory jurisdiction for this institution
454        pub fn regulatory_jurisdiction(&self) -> &'static str {
455            match self.country_code() {
456                "US" => "Federal Reserve / OCC / FDIC",
457                "GB" => "Bank of England / PRA / FCA",
458                "DE" => "BaFin / ECB",
459                "FR" => "ACPR / ECB",
460                "JP" => "JFSA / Bank of Japan",
461                "CH" => "FINMA / SNB",
462                "CA" => "OSFI / Bank of Canada",
463                "AU" => "APRA / RBA",
464                "SG" => "MAS",
465                "HK" => "HKMA",
466                "CN" => "PBOC / CBIRC",
467                "IN" => "RBI",
468                "BR" => "Central Bank of Brazil",
469                "MX" => "CNBV / Banxico",
470                _ => "Other National Authority",
471            }
472        }
473
474        /// Get a human-readable description of this BIC
475        pub fn description(&self) -> String {
476            format!(
477                "Bank: {} | Country: {} | Location: {} | Branch: {}",
478                self.bank_code(),
479                self.country_code(),
480                self.location_code(),
481                self.branch_code().unwrap_or("XXX (Head Office)")
482            )
483        }
484    }
485
486    impl std::fmt::Display for BIC {
487        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488            write!(f, "{}", self.value)
489        }
490    }
491
492    impl std::str::FromStr for BIC {
493        type Err = ParseError;
494
495        fn from_str(s: &str) -> Result<Self, Self::Err> {
496            Self::parse(s, None)
497        }
498    }
499
500    impl From<BIC> for String {
501        fn from(bic: BIC) -> String {
502            bic.value
503        }
504    }
505
506    impl AsRef<str> for BIC {
507        fn as_ref(&self) -> &str {
508            &self.value
509        }
510    }
511
512    /// SWIFT Currency Code (ISO 4217)
513    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
514    pub struct Currency {
515        pub code: String,
516    }
517
518    impl Currency {
519        pub fn new(code: String) -> Self {
520            Self {
521                code: code.to_uppercase(),
522            }
523        }
524    }
525
526    /// SWIFT Amount with decimal handling
527    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
528    pub struct Amount {
529        pub value: String,
530        pub decimal_places: u8,
531    }
532
533    impl Amount {
534        pub fn new(value: String) -> Self {
535            let decimal_places = if value.contains(',') {
536                value.split(',').nth(1).map(|s| s.len() as u8).unwrap_or(0)
537            } else {
538                0
539            };
540
541            Self {
542                value,
543                decimal_places,
544            }
545        }
546
547        pub fn to_decimal(&self) -> Result<f64, std::num::ParseFloatError> {
548            self.value.replace(',', ".").parse()
549        }
550    }
551
552    #[cfg(test)]
553    mod tests {
554        use super::*;
555
556        #[test]
557        fn test_bic_creation() {
558            let bic = BIC::new("CHASUS33XXX").unwrap();
559            assert_eq!(bic.value(), "CHASUS33XXX");
560            assert_eq!(bic.bank_code(), "CHAS");
561            assert_eq!(bic.country_code(), "US");
562            assert_eq!(bic.location_code(), "33");
563            assert_eq!(bic.branch_code(), Some("XXX"));
564            assert!(bic.is_full_bic());
565        }
566
567        #[test]
568        fn test_bic_short_format() {
569            let bic = BIC::new("DEUTDEFF").unwrap();
570            assert_eq!(bic.value(), "DEUTDEFF");
571            assert_eq!(bic.bank_code(), "DEUT");
572            assert_eq!(bic.country_code(), "DE");
573            assert_eq!(bic.location_code(), "FF");
574            assert_eq!(bic.branch_code(), None);
575            assert!(!bic.is_full_bic());
576        }
577
578        #[test]
579        fn test_bic_validation_errors() {
580            // Too short
581            assert!(BIC::new("CHAS").is_err());
582
583            // Too long
584            assert!(BIC::new("CHASUS33XXXX").is_err());
585
586            // Invalid bank code (numeric)
587            assert!(BIC::new("1234US33").is_err());
588
589            // Invalid country code (numeric)
590            assert!(BIC::new("CHAS22XX").is_err());
591
592            // Invalid location code (special chars)
593            assert!(BIC::new("CHASUS@#").is_err());
594        }
595
596        #[test]
597        fn test_bic_case_normalization() {
598            let bic = BIC::new("chasus33xxx").unwrap();
599            assert_eq!(bic.value(), "CHASUS33XXX");
600        }
601
602        #[test]
603        fn test_bic_utilities() {
604            let bic = BIC::new("CHASUS33XXX").unwrap();
605            assert!(bic.is_major_financial_center());
606            assert!(bic.is_retail_bank());
607            assert!(bic.supports_real_time_payments());
608            assert_eq!(
609                bic.regulatory_jurisdiction(),
610                "Federal Reserve / OCC / FDIC"
611            );
612        }
613
614        #[test]
615        fn test_bic_display_and_description() {
616            let bic = BIC::new("CHASUS33XXX").unwrap();
617            assert_eq!(bic.to_string(), "CHASUS33XXX");
618            assert!(bic.description().contains("CHAS"));
619            assert!(bic.description().contains("US"));
620        }
621
622        #[test]
623        fn test_bic_from_str() {
624            let bic: BIC = "CHASUS33XXX".parse().unwrap();
625            assert_eq!(bic.value(), "CHASUS33XXX");
626        }
627    }
628}
629
630/// Enumeration of all supported SWIFT message types for automatic parsing
631#[derive(Debug, Clone, Serialize)]
632#[serde(tag = "message_type")]
633pub enum ParsedSwiftMessage {
634    #[serde(rename = "103")]
635    MT103(Box<SwiftMessage<messages::MT103>>),
636    #[serde(rename = "202")]
637    MT202(Box<SwiftMessage<messages::MT202>>),
638}
639
640impl ParsedSwiftMessage {
641    /// Get the message type as a string
642    pub fn message_type(&self) -> &'static str {
643        match self {
644            ParsedSwiftMessage::MT103(_) => "103",
645            ParsedSwiftMessage::MT202(_) => "202",
646        }
647    }
648
649    /// Convert to a specific message type if it matches
650    pub fn as_mt103(&self) -> Option<&SwiftMessage<messages::MT103>> {
651        match self {
652            ParsedSwiftMessage::MT103(msg) => Some(msg),
653            _ => None,
654        }
655    }
656
657    pub fn as_mt202(&self) -> Option<&SwiftMessage<messages::MT202>> {
658        match self {
659            ParsedSwiftMessage::MT202(msg) => Some(msg),
660            _ => None,
661        }
662    }
663
664    /// Convert into a specific message type if it matches
665    pub fn into_mt103(self) -> Option<SwiftMessage<messages::MT103>> {
666        match self {
667            ParsedSwiftMessage::MT103(msg) => Some(*msg),
668            _ => None,
669        }
670    }
671
672    pub fn into_mt202(self) -> Option<SwiftMessage<messages::MT202>> {
673        match self {
674            ParsedSwiftMessage::MT202(msg) => Some(*msg),
675            _ => None,
676        }
677    }
678}
679
680#[cfg(test)]
681mod tests {
682    use super::*;
683    use crate::messages::mt103::MT103;
684
685    #[test]
686    fn test_full_mt103_parsing() {
687        let raw_message = r#"{1:F01BNPAFRPPXXX0000000000}{2:O1031234240101DEUTDEFFXXXX12345678952401011234N}{3:{103:EBA}}{4:
688:20:FT21001234567890
689:23B:CRED
690:32A:240101USD1000,00
691:50K:/1234567890
692ACME CORPORATION
693123 MAIN STREET
694NEW YORK NY 10001
695:52A:BNPAFRPPXXX
696:57A:DEUTDEFFXXX
697:59:/DE89370400440532013000
698MUELLER GMBH
699HAUPTSTRASSE 1
70010115 BERLIN
701:70:PAYMENT FOR INVOICE 12345
702:71A:OUR
703-}"#;
704
705        let result = SwiftParser::parse::<MT103>(raw_message);
706        assert!(result.is_ok(), "Parsing should succeed: {:?}", result.err());
707
708        let parsed = result.unwrap();
709        assert_eq!(parsed.message_type, "103");
710
711        // Test JSON serialization
712        let json = serde_json::to_string_pretty(&parsed);
713        assert!(json.is_ok(), "JSON serialization should work");
714        println!("Parsed MT103 JSON:\n{}", json.unwrap());
715    }
716
717    #[test]
718    fn test_field_parsing() {
719        use crate::fields::field20::Field20;
720
721        let result = Field20::parse(":20:FT21001234567890");
722        assert!(result.is_ok());
723
724        let field = result.unwrap();
725        assert_eq!(field.to_swift_string(), ":20:FT21001234567890");
726    }
727}