Matching Postal Codes with Regular Expressions
Postal-code validation is one of the most common form-validation tasks in ecommerce, shipping, and address-collection UIs. Every country has its own format — and even within one country, the format has often evolved (US ZIP+4, UK district-plus-sector). Below are the patterns developers reach for every day, with country-specific rules and worked examples.
The Core Patterns
United States
/^\d{5}$/ // Plain 5-digit zip
/^\d{5}(-\d{4})?$/ // ZIP+4 (dash optional)
/^\d{5}([\s-]\d{4})?$/ // ZIP+4 allowing space OR dash
/^[0-9]{5}(?:-[0-9]{4})?$/ // Same, non-capturing groupThe US Postal Service issues ZIP codes as 5 digits, with an optional 4-digit extension (ZIP+4) identifying a specific delivery point. The dash is standard; some databases store the ZIP+4 as 9 straight digits — validate accordingly.
Canada
/^[A-Za-z]\d[A-Za-z] ?\d[A-Za-z]\d$/ // K1A 0B1 or K1A0B1
/^[ABCEGHJ-NPRSTVXY]\d[A-Z] ?\d[A-Z]\d$/i // Excludes D, F, I, O, Q, UCanadian postal codes are 6 characters in the pattern A1A 1A1. Canada Post never uses the letters D, F, I, O, Q, or U in the first position — the stricter pattern above rejects any address using these invalid letters.
United Kingdom
/^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i
// Matches: SW1A 1AA, EC1A 1BB, M1 1AE, B33 8TH, DN55 1PT, W1A 0AXUK postcodes have 5 subformats. The regex above covers all of them at ~99% accuracy. For 100% Royal Mail-compliant validation, use the official long-form regex — it's 100+ characters but rejects invalid combinations like Z1 1AA.
India (PIN Code)
/^[1-9]\d{5}$/ // 6 digits, first digit 1-9 (no leading zero)
/^\d{6}$/ // Loose: any 6 digitsOther Common Countries
Germany: /^\d{5}$/
France: /^\d{5}$/
Japan: /^\d{3}-?\d{4}$/ // 100-0001 or 1000001
Australia: /^\d{4}$/
Brazil: /^\d{5}-?\d{3}$/ // 01310-100 or 01310100
Mexico: /^\d{5}$/
Netherlands: /^\d{4} ?[A-Z]{2}$/i // 1012 AB or 1012ABLanguage-Specific Usage
JavaScript
const US_ZIP = /^\d{5}(-\d{4})?$/;
function isUsZip(str) {
return US_ZIP.test(str);
}
isUsZip('90210'); // true
isUsZip('12345-6789'); // true
isUsZip('1234'); // false
isUsZip('123456'); // false
// Extract every US zip code from a text block
'Ship from 90210 to 10001-1234'.match(/\b\d{5}(-\d{4})?\b/g);
// → ['90210', '10001-1234']Python
import re
US_ZIP = re.compile(r'^\d{5}(-\d{4})?$')
CA_POSTAL = re.compile(r'^[A-Za-z]\d[A-Za-z] ?\d[A-Za-z]\d$')
def validate_postal(country: str, code: str) -> bool:
if country == 'US':
return bool(US_ZIP.match(code))
if country == 'CA':
return bool(CA_POSTAL.match(code))
return False
validate_postal('US', '90210') # True
validate_postal('CA', 'K1A 0B1') # TruePHP
function isUsZip(string $s): bool {
return (bool) preg_match('/^\d{5}(-\d{4})?$/', $s);
}
// Normalise a ZIP+4 to always include the dash
function normalizeZip(string $s): ?string {
if (preg_match('/^(\d{5})[-\s]?(\d{4})?$/', $s, $m)) {
return isset($m[2]) && $m[2] ? "{$m[1]}-{$m[2]}" : $m[1];
}
return null;
}Common Pitfalls
Regex doesn't verify the code exists
/^\d5$/ accepts 00000 and 99999 — both syntactically valid but likely not real addresses. For shipping-critical apps, always follow regex validation with a call to a postal-lookup API (USPS Address Validation, Google Address Validation).
US ZIP with leading zeros
Some New England zip codes start with 0 (e.g. 02108Boston). If your input parses zips as integers you'll lose the leading zero and produce a 4-digit number. Always store zips as strings.
Canadian postal codes and reserved letters
The letters D, F, I, O, Q, and U are never used as the first character of a Canadian postal code. A tight pattern rejects them; a loose [A-Za-z] accepts them and produces false positives.
UK postcodes are messier than they look
The full Royal Mail regex is over 100 characters because certain letter combinations are invalid in certain positions. A short regex covers most cases but will accept a few impossible codes. For shipping-critical UK forms, use PAF (Postcode Address File) integration.
Postal Code Cheatsheet
| Country | Pattern | Example |
|---|---|---|
| US | /^\d{5}(-\d{4})?$/ | 90210, 12345-6789 |
| Canada | /^[A-Za-z]\d[A-Za-z] ?\d[A-Za-z]\d$/ | K1A 0B1 |
| UK | /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i | SW1A 1AA |
| India | /^[1-9]\d{5}$/ | 110001 |
| Netherlands | /^\d{4} ?[A-Z]{2}$/i | 1012 AB |
Testing Your Postal Code Regex
Use the live Regex Tester above with these test strings:
- US match:
90210,02108,12345-6789 - Canada match:
K1A 0B1,M5V 3A8 - UK match:
SW1A 1AA,M1 1AE,B33 8TH - Reject:
1234,ABCDE,12345-67, empty - Edge: Some databases store ZIP+4 as
123456789(no dash) — decide up front
Common Mistakes When Writing Postal Code Regex
A few recurring pitfalls trip up developers building international address forms:
- Assuming everyone uses 5 digits. Canada, UK, Netherlands, and Japan all use mixed letter/digit formats. Ship a country selector or fall back to a permissive validator.
- Storing zips as integers. Boston zips start with 0. Storing them as int drops the leading zero and breaks address matching. Always store as strings.
- Requiring the space in Canada/UK codes. Users often type
K1A0B1orSW1A1AAwithout the space. Make the space optional:[ ]?. - Not normalising before storage.
k1a 0b1,K1A 0B1, andK1A0B1refer to the same address. Uppercase and strip/normalize the space before storing so lookups match. - Treating regex validation as authoritative. Regex only checks format. For shipping-critical use, follow with a real postal-database lookup.
Performance Notes
Postal-code regexes are extremely fast — all quantifiers are small and bounded ({5},{4}), so there's no backtracking risk. Even validating millions of address rows in a single pass takes tens of milliseconds. The main perf trap in international address forms is running EVERY country regex on EVERY input instead of dispatching on the country selector — a naive setup does 20-30 regex checks per keystroke. Look up the country's pattern from a map and apply just that one.