Regex & parsing · 9 min lezen
Nederlandse postcode-regex — edge cases in JavaScript, Python, PHP, Go en C#
Door Alex · Gepubliceerd 17 april 2026
Een Nederlandse postcode lijkt simpel: vier cijfers, een spatie, twee letters. Ondertussen bestaan er minstens zes manieren waarop een gebruiker hem intypt (lowercase, zonder spatie, met non-breaking space, met leestekens, met voorloopnul), en minstens één combinatie die PostNL bewust uitsluit. Dit artikel bundelt de exacte regex-varianten, geeft werkende implementaties in vijf talen en sluit af met een testset die elke edge-case afdekt.
1. Het Nederlandse postcode-formaat
Volgens PostNL bestaat een postcode uit vier cijfers gevolgd door twee hoofdletters. Het eerste cijfer is nooit een nul (dat levert 1000–9999 als cijfergedeelte op). De twee letters zijn hoofdletters A–Z. Tussen het cijferdeel en het letterdeel staat in de officiële schrijfwijze één gewone spatie.
In praktijk accepteren de meeste systemen ook geen spatie en kleine letters — normalisatie vóór opslag is bijna altijd de juiste aanpak. De validatie is dan "lenient", de opslag is strict.
2. De SA/SD/SS-uitzondering
PostNL gebruikt drie lettercombinaties niet: SA, SD en SS. De reden is historisch — associatie met SS als lettercombinatie van de Schutzstaffel, en SA/SD als verwante organisaties. Je zult dus nooit een postcode-leaf krijgen die eindigt op één van die drie combinaties. Voor validatie betekent dit dat strikte regex deze combinaties kan uitsluiten, maar voor praktisch gebruik is het zelden de moeite: PostNL geeft ze nooit uit, dus een gebruiker die ze invoert heeft sowieso een fictieve of foutieve postcode.
De meeste productievalidaties laten deze uitsluiting daarom weg (de regex wordt er lelijker van, en je laadt een historische uitzondering op in je codebase). Voor wie het wel wil afdwingen, staat hieronder een variant met negatieve lookahead.
3. Regex patterns
| Variant | Pattern |
|---|---|
| Lenient (case-insensitive, spatie optioneel) | /^[1-9][0-9]{3}\s?[A-Za-z]{2}$/ |
| Strict (hoofdletters, spatie verplicht) | /^[1-9][0-9]{3} [A-Z]{2}$/ |
| Strict zonder SA/SD/SS | /^[1-9][0-9]{3} (?!SA|SD|SS)[A-Z]{2}$/ |
De lenient-variant is wat je aan de invoerkant gebruikt (je accepteert wat de gebruiker typt), de strict-variant is wat je na normalisatie verwacht bij opslag of externe API-calls. Een goed ontworpen formulier doet allebei: lenient matchen, normaliseren, strict hermatchen als assertion.
4. Implementatie per taal
JavaScript / TypeScript
const POSTCODE_LENIENT = /^[1-9][0-9]{3}\s?[A-Za-z]{2}$/;
const POSTCODE_STRICT = /^[1-9][0-9]{3} [A-Z]{2}$/;
export function isValidPostcode(input: string): boolean {
return POSTCODE_LENIENT.test(input.trim());
}
export function normalizePostcode(input: string): string | null {
const trimmed = input.trim().replace(/\s+/g, '');
const match = /^([1-9][0-9]{3})([A-Za-z]{2})$/.exec(trimmed);
if (!match) return null;
return `${match[1]} ${match[2].toUpperCase()}`;
}Python
import re
_LENIENT = re.compile(r"^[1-9][0-9]{3}\s?[A-Za-z]{2}$")
_NORMALIZE = re.compile(r"^([1-9][0-9]{3})([A-Za-z]{2})$")
def is_valid_postcode(value: str) -> bool:
return bool(_LENIENT.match(value.strip()))
def normalize_postcode(value: str) -> str | None:
compact = re.sub(r"\s+", "", value.strip())
match = _NORMALIZE.match(compact)
if not match:
return None
return f"{match.group(1)} {match.group(2).upper()}"PHP
<?php
function isValidPostcode(string $input): bool {
return (bool) preg_match('/^[1-9][0-9]{3}\s?[A-Za-z]{2}$/', trim($input));
}
function normalizePostcode(string $input): ?string {
$compact = preg_replace('/\s+/', '', trim($input));
if (!preg_match('/^([1-9][0-9]{3})([A-Za-z]{2})$/', $compact, $m)) {
return null;
}
return $m[1] . ' ' . strtoupper($m[2]);
}Go
package postcode
import (
"regexp"
"strings"
)
var (
lenient = regexp.MustCompile(`^[1-9][0-9]{3}\s?[A-Za-z]{2}$`)
normalize = regexp.MustCompile(`^([1-9][0-9]{3})([A-Za-z]{2})$`)
ws = regexp.MustCompile(`\s+`)
)
func IsValid(input string) bool {
return lenient.MatchString(strings.TrimSpace(input))
}
func Normalize(input string) (string, bool) {
compact := ws.ReplaceAllString(strings.TrimSpace(input), "")
m := normalize.FindStringSubmatch(compact)
if m == nil {
return "", false
}
return m[1] + " " + strings.ToUpper(m[2]), true
}C#
using System.Text.RegularExpressions;
public static class Postcode
{
private static readonly Regex Lenient = new(
@"^[1-9][0-9]{3}\s?[A-Za-z]{2}$",
RegexOptions.Compiled);
private static readonly Regex NormalizePattern = new(
@"^([1-9][0-9]{3})([A-Za-z]{2})$",
RegexOptions.Compiled);
public static bool IsValid(string input) =>
Lenient.IsMatch(input.Trim());
public static string? Normalize(string input)
{
var compact = Regex.Replace(input.Trim(), @"\s+", "");
var m = NormalizePattern.Match(compact);
return m.Success ? $"{m.Groups[1].Value} {m.Groups[2].Value.ToUpper()}" : null;
}
}Alle vijf de implementaties volgen dezelfde tweestappen-strategie: match lenient, normaliseer vervolgens. Dat is robuuster dan één regex die alles in één keer doet, omdat het error messages specifieker maakt (je kunt uitleggen water mis is in plaats van alleen "geen match").
5. Testcases
| Invoer | Lenient | Strict | Normalize |
|---|---|---|---|
| 1234 AB | valid | valid | 1234 AB |
| 1234AB | valid | invalid | 1234 AB |
| 1234ab | valid | invalid | 1234 AB |
| 1234 AB | invalid (dubbele spatie) | invalid | 1234 AB |
| 0234 AB | invalid (begint met 0) | invalid | null |
| 1234 SA | valid (lenient accepteert) | valid (zonder SA/SD/SS-regex) | 1234 SA |
| 12345 AB | invalid | invalid | null |
| 1234 A | invalid | invalid | null |
| 1234 AB1 | invalid | invalid | null |
Let op rij 4 (dubbele spatie): de lenient-regex met \s? accepteert 0 of 1 spatie, niet 2. Omdat de normalize-functie eerst alle whitespace strippt, krijgt de gebruiker alsnog een geldig resultaat — de combinatie van lenient match + normalize is dus pragmatischer dan alleen een regex.
6. Formateren en normaliseren
De canonieke weergave is 1234 AB: hoofdletters, één gewone spatie (U+0020). De drie stappen die je wilt toepassen in elke volgorde — ze commuteren:
- Trim externe whitespace. Gebruikers plakken vaak per ongeluk een leading/trailing spatie mee.
- Strippen van alle interne whitespace. Non-breaking spaces (U+00A0), tabs en dubbele spaties verdwijnen. Pas daarna plaats je zelf de ene juiste spatie terug.
- Letters naar hoofdletters. Gebruik locale-onafhankelijke uppercase waar dat mogelijk is — voor postcodes is dat geen probleem omdat het alfabet A–Z geen Turkse i-variant of vergelijkbare locale-sensitiviteit heeft, maar het is een goede gewoonte.
Bewust níet meenemen: de SA/SD/SS-filter. Die hoort thuis in de validatie-laag, niet in de normalisatie-laag. Normalisatie moet idempotent en verliesvrij zijn — filteren is dat niet.
7. Bronnen
- PostNL open data — postcode-tabel (CSV) met alle uitgegeven postcodes en gemeenten.
- CBS — Wijk- en Buurtkaart, officiële administratieve koppeling tussen postcodes en buurten.
- Kadaster BAG (Basisregistraties Adressen en Gebouwen) — brondata voor koppeling postcode + huisnummer naar adres.
Gerelateerde tools
- Postcode Generator — genereer fictieve NL-postcodes die aan de regex voldoen
- Dataset Generator — postcodes in combinatie met namen, adressen en andere velden
- De elfproef ontleed — validatie van een ander Nederlands identifier-formaat
Nieuwe artikelen in je inbox
Max. 1 mail per maand. Geen spam. Uitschrijven in 1 klik.