Regex & parsing · 9 min lezen

Nederlandse postcode-regex — edge cases in JavaScript, Python, PHP, Go en C#

Door Alex · Gepubliceerd 17 april 2026

Deel:

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

VariantPattern
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

InvoerLenientStrictNormalize
1234 ABvalidvalid1234 AB
1234ABvalidinvalid1234 AB
1234abvalidinvalid1234 AB
1234 ABinvalid (dubbele spatie)invalid1234 AB
0234 ABinvalid (begint met 0)invalidnull
1234 SAvalid (lenient accepteert)valid (zonder SA/SD/SS-regex)1234 SA
12345 ABinvalidinvalidnull
1234 Ainvalidinvalidnull
1234 AB1invalidinvalidnull

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:

  1. Trim externe whitespace. Gebruikers plakken vaak per ongeluk een leading/trailing spatie mee.
  2. 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.
  3. 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

Nieuwe artikelen in je inbox

Max. 1 mail per maand. Geen spam. Uitschrijven in 1 klik.