From 5994f4ea1b8bbbd5efa53b91541bcddd92b90530 Mon Sep 17 00:00:00 2001 From: Fira Curie Date: Tue, 14 Oct 2025 11:31:13 +0200 Subject: [PATCH 1/4] feat: add test to check for bic that's only valid under 2022 specification --- validator_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/validator_test.go b/validator_test.go index 25b0ef80..666b44eb 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13212,6 +13212,7 @@ func TestBicIsoFormatValidation(t *testing.T) { {"SBICKEN1", "bic", true}, {"SBICKENY", "bic", true}, {"SBICKEN1YYP", "bic", true}, + {"E097AEXX", "bic", true}, // valid under https://www.iso.org/standard/84108.html {"SBIC23NXXX", "bic", false}, {"S23CKENXXXX", "bic", false}, {"SBICKENXX", "bic", false}, From 664102b4735f1081473fdd8c167c1ed6aed4dc6c Mon Sep 17 00:00:00 2001 From: Fira Curie Date: Tue, 14 Oct 2025 11:32:20 +0200 Subject: [PATCH 2/4] feat: introduce regex compliant with iso 9362:2022 --- regexes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regexes.go b/regexes.go index 0b3615f5..0a97037d 100644 --- a/regexes.go +++ b/regexes.go @@ -68,7 +68,7 @@ const ( hTMLRegexString = `<[/]?([a-zA-Z]+).*?>` jWTRegexString = "^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$" splitParamsRegexString = `'[^']*'|\S+` - bicRegexString = `^[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?$` + bicRegexString = `^(?:[A-Z]{4}|[A-Z][0-9]{3})[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$` semverRegexString = `^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$` // numbered capture groups https://semver.org/ dnsRegexStringRFC1035Label = "^[a-z]([-a-z0-9]*[a-z0-9])?$" cveRegexString = `^CVE-(1999|2\d{3})-(0[^0]\d{2}|0\d[^0]\d{1}|0\d{2}[^0]|[1-9]{1}\d{3,})$` // CVE Format Id https://cve.mitre.org/cve/identifiers/syntaxchange.html From a74c100e03da3bad27618269bf8f3f78e5d0433f Mon Sep 17 00:00:00 2001 From: Fira Curie Date: Tue, 14 Oct 2025 13:37:30 +0200 Subject: [PATCH 3/4] fix: revert to old validation regex and add more test cases --- regexes.go | 2 +- validator_test.go | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/regexes.go b/regexes.go index 0a97037d..ae2ec98f 100644 --- a/regexes.go +++ b/regexes.go @@ -68,7 +68,7 @@ const ( hTMLRegexString = `<[/]?([a-zA-Z]+).*?>` jWTRegexString = "^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$" splitParamsRegexString = `'[^']*'|\S+` - bicRegexString = `^(?:[A-Z]{4}|[A-Z][0-9]{3})[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$` + bicRegexString = `^[A-Z0-9]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$` semverRegexString = `^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$` // numbered capture groups https://semver.org/ dnsRegexStringRFC1035Label = "^[a-z]([-a-z0-9]*[a-z0-9])?$" cveRegexString = `^CVE-(1999|2\d{3})-(0[^0]\d{2}|0\d[^0]\d{1}|0\d{2}[^0]|[1-9]{1}\d{3,})$` // CVE Format Id https://cve.mitre.org/cve/identifiers/syntaxchange.html diff --git a/validator_test.go b/validator_test.go index 666b44eb..bcb48ba8 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13214,11 +13214,44 @@ func TestBicIsoFormatValidation(t *testing.T) { {"SBICKEN1YYP", "bic", true}, {"E097AEXX", "bic", true}, // valid under https://www.iso.org/standard/84108.html {"SBIC23NXXX", "bic", false}, - {"S23CKENXXXX", "bic", false}, + {"S23CKENXXXX", "bic", true}, {"SBICKENXX", "bic", false}, {"SBICKENXX9", "bic", false}, {"SBICKEN13458", "bic", false}, {"SBICKEN", "bic", false}, + {"DEUTDEFF", "bic", true}, // 8-char classic (Germany) + {"DEUTDEFF500", "bic", true}, // 11-char with numeric branch + {"A1B2US33", "bic", true}, // digits allowed in 4!c (bank code) + {"1234US33", "bic", true}, // all digits in 4!c (2022) + {"ZZZ1USAA", "bic", true}, // mixed alnum bank + alnum location + {"AB12AE00", "bic", true}, // UAE 8-char + {"AB12AE009Z9", "bic", true}, // UAE 11-char with mixed branch + {"WG11US335AB", "bic", true}, // example-style with digits in branch + {"BNPAFRPP", "bic", true}, // France (BNP Paribas style) + {"BOFAUS3NXXX", "bic", true}, // US with default XXX branch + {"HSBCHKHHXXX", "bic", true}, // Hong Kong, default branch + {"NEDSZAJJ", "bic", true}, // South Africa 8-char + {"BARCGB22", "bic", true}, // GB 8-char + {"BARCGB22XXX", "bic", true}, // GB 11-char with XXX branch + {"0000GB00", "bic", true}, // 4!c all digits + 2!c all digits (allowed) + {"A1B2GB00XXX", "bic", true}, // valid 11-char with numeric location and XXX + {"TATRAEBX", "bic", true}, // UAE 8-char + {"TATRSABX", "bic", true}, // Saudi 8-char + {"TATREGBX", "bic", true}, // Egypt 8-char + {"TATRBHBX", "bic", true}, // Bahrain 8-char + + {"DEUTDEFFF", "bic", false}, // 9-char (invalid length) + {"DEUTDEFF5", "bic", false}, // 9-char (invalid length) + {"DEUTDE", "bic", false}, // 6-char (invalid length) + {"DEUTDEFF50", "bic", false}, // 10-char (invalid length) + {"DEUTDEFF5000", "bic", false}, // 12-char (invalid length) + {"deUTDEFF", "bic", false}, // lowercase not allowed + {"DEUTDEfF", "bic", false}, // lowercase in location + {"DEU@DEFF", "bic", false}, // special char in bank + {"ABCD12FF", "bic", false}, // digits in 2!a country (invalid) + {"ABCDDE1-", "bic", false}, // hyphen in location + {"ABCDDE1_", "bic", false}, // underscore in location + {"ABCDDE١٢", "bic", false}, // non-ASCII digits in location } validate := New() From 49e105320e33438d7c513d97f07ee3d4833068db Mon Sep 17 00:00:00 2001 From: Fira Curie Date: Sun, 26 Oct 2025 08:04:27 +0100 Subject: [PATCH 4/4] feat: separate 2014 and 2022 bic implementations, update README to reflect changes --- README.md | 3 ++- baked_in.go | 16 ++++++++++++---- regexes.go | 6 ++++-- validator_test.go | 42 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cb5d4194..d3f7f2cc 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,8 @@ validate := validator.New(validator.WithRequiredStructEnabled()) | base64 | Base64 String | | base64url | Base64URL String | | base64rawurl | Base64RawURL String | -| bic | Business Identifier Code (ISO 9362) | +| bic_iso_9362_2014 | Business Identifier Code (ISO 9362:2014) | +| bic | Business Identifier Code (ISO 9362:2022) | | bcp47_language_tag | Language tag (BCP 47) | | btc_addr | Bitcoin Address | | btc_addr_bech32 | Bitcoin Bech32 Address (segwit) | diff --git a/baked_in.go b/baked_in.go index 8fd55e77..0ae59f0b 100644 --- a/baked_in.go +++ b/baked_in.go @@ -237,7 +237,8 @@ var ( "bcp47_language_tag": isBCP47LanguageTag, "postcode_iso3166_alpha2": isPostcodeByIso3166Alpha2, "postcode_iso3166_alpha2_field": isPostcodeByIso3166Alpha2Field, - "bic": isIsoBicFormat, + "bic_iso_9362_2014": isIsoBic2014Format, + "bic": isIsoBic2022Format, "semver": isSemverFormat, "dns_rfc1035_label": isDnsRFC1035LabelFormat, "credit_card": isCreditCard, @@ -2943,11 +2944,18 @@ func isBCP47LanguageTag(fl FieldLevel) bool { panic(fmt.Sprintf("Bad field type %s", field.Type())) } -// isIsoBicFormat is the validation function for validating if the current field's value is a valid Business Identifier Code (SWIFT code), defined in ISO 9362 -func isIsoBicFormat(fl FieldLevel) bool { +// isIsoBic2014Format is the validation function for validating if the current field's value is a valid Business Identifier Code (SWIFT code), defined in ISO 9362 2014 +func isIsoBic2014Format(fl FieldLevel) bool { bicString := fl.Field().String() - return bicRegex().MatchString(bicString) + return bic2014Regex().MatchString(bicString) +} + +// isIsoBic2022Format is the validation function for validating if the current field's value is a valid Business Identifier Code (SWIFT code), defined in ISO 9362 2022 +func isIsoBic2022Format(fl FieldLevel) bool { + bicString := fl.Field().String() + + return bic2022Regex().MatchString(bicString) } // isSemverFormat is the validation function for validating if the current field's value is a valid semver version, defined in Semantic Versioning 2.0.0 diff --git a/regexes.go b/regexes.go index ae2ec98f..64290bb3 100644 --- a/regexes.go +++ b/regexes.go @@ -68,7 +68,8 @@ const ( hTMLRegexString = `<[/]?([a-zA-Z]+).*?>` jWTRegexString = "^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$" splitParamsRegexString = `'[^']*'|\S+` - bicRegexString = `^[A-Z0-9]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$` + bic2014RegexString = `^[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?$` + bic2022RegexString = `^[A-Z0-9]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$` semverRegexString = `^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$` // numbered capture groups https://semver.org/ dnsRegexStringRFC1035Label = "^[a-z]([-a-z0-9]*[a-z0-9])?$" cveRegexString = `^CVE-(1999|2\d{3})-(0[^0]\d{2}|0\d[^0]\d{1}|0\d{2}[^0]|[1-9]{1}\d{3,})$` // CVE Format Id https://cve.mitre.org/cve/identifiers/syntaxchange.html @@ -153,7 +154,8 @@ var ( hTMLRegex = lazyRegexCompile(hTMLRegexString) jWTRegex = lazyRegexCompile(jWTRegexString) splitParamsRegex = lazyRegexCompile(splitParamsRegexString) - bicRegex = lazyRegexCompile(bicRegexString) + bic2014Regex = lazyRegexCompile(bic2014RegexString) + bic2022Regex = lazyRegexCompile(bic2022RegexString) semverRegex = lazyRegexCompile(semverRegexString) dnsRegexRFC1035Label = lazyRegexCompile(dnsRegexStringRFC1035Label) cveRegex = lazyRegexCompile(cveRegexString) diff --git a/validator_test.go b/validator_test.go index bcb48ba8..cad80e1a 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13202,7 +13202,47 @@ func TestBCP47LanguageTagValidation(t *testing.T) { }, "Bad field type int") } -func TestBicIsoFormatValidation(t *testing.T) { +func TestBicIso2014FormatValidation(t *testing.T) { + tests := []struct { + value string `validate:"bic_iso_9362_2014"` + tag string + expected bool + }{ + {"SBICKEN1345", "bic_iso_9362_2014", true}, + {"SBICKEN1", "bic_iso_9362_2014", true}, + {"SBICKENY", "bic_iso_9362_2014", true}, + {"SBICKEN1YYP", "bic_iso_9362_2014", true}, + {"SBIC23NXXX", "bic_iso_9362_2014", false}, + {"S23CKENXXXX", "bic_iso_9362_2014", false}, + {"SBICKENXX", "bic_iso_9362_2014", false}, + {"SBICKENXX9", "bic_iso_9362_2014", false}, + {"SBICKEN13458", "bic_iso_9362_2014", false}, + {"SBICKEN", "bic_iso_9362_2014", false}, + } + + validate := New() + + for i, test := range tests { + errs := validate.Var(test.value, test.tag) + + if test.expected { + if !IsEqual(errs, nil) { + t.Fatalf("Index: %d bic_iso_9362_2014 failed Error: %s", i, errs) + } + } else { + if IsEqual(errs, nil) { + t.Fatalf("Index: %d bic_iso_9362_2014 failed Error: %s", i, errs) + } else { + val := getError(errs, "", "") + if val.Tag() != "bic_iso_9362_2014" { + t.Fatalf("Index: %d bic_iso_9362_2014 failed Error: %s", i, errs) + } + } + } + } +} + +func TestBicIso2022FormatValidation(t *testing.T) { tests := []struct { value string `validate:"bic"` tag string