validators

package module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 20, 2026 License: MIT Imports: 13 Imported by: 1

README

validators

A Go input validation library designed for both humans and AI agents. Every validation returns structured, machine-readable error codes alongside human-readable messages — so your UI, your logs, and your AI agent all get exactly what they need from a single call.

result := validators.IBAN("DE0037040044053201300")
if !result.Valid {
    // Human: result.Errors[0].Message → "IBAN length for DE must be 22, got 21"
    // Agent: result.Errors[0].Code    → "iban.length.mismatch"
    //        result.Errors[0].Context → {"country_code": "DE", "expected_length": 22, "actual_length": 21}
}

Install

go get github.com/laenen-partners/validators

Validators

Tier 1 — Core
Validator Function Description
Email Email(value, checkMX) RFC 5321 format, optional MX lookup
IBAN IBAN(value) Country length table, MOD-97 checksum
SWIFT/BIC SWIFT(value) 8 or 11 char bank identifier codes
Phone Phone(value) E.164 format, country-specific length rules (90+ countries)
Credit Card CreditCard(value) Luhn checksum, network detection (Visa, Mastercard, Amex, etc.)
VAT VAT(value) EU VAT numbers for 28 countries + Switzerland
URL URL(value) Scheme + host validation, metadata extraction
UUID UUID(value, version) Format and optional version check (v1–v5)
Tier 2 — Standard
Validator Function Description
Postal Code PostalCode(value, countryCode) Country-specific formats (40+ countries)
Country Code CountryCode(value) ISO 3166-1 alpha-2 and alpha-3
Currency Code CurrencyCode(value) ISO 4217 three-letter codes
Date Date(value) ISO 8601 (YYYY-MM-DD), calendar correctness including leap years
IPv4 IPv4(value) Dotted-decimal, leading zero rejection
IPv6 IPv6(value) Full, compressed, and mixed formats
Domain Domain(value) RFC 1035 label rules, TLD validation
LEI LEI(value) Legal Entity Identifier, MOD-97 checksum
Tier 3 — Extended
Validator Function Description
Semantic Version SemVer(value) major.minor.patch with optional prerelease and build metadata
CIDR CIDR(value) IP + subnet prefix for IPv4 and IPv6
MAC Address MAC(value) 48-bit in colon, hyphen, or dot notation
ISBN ISBN(value) ISBN-10 (mod-11) and ISBN-13 (mod-10) checksums
CRON CRON(value) 5-field cron expressions with range/step/list validation
JWT JWT(value) Three-part base64url structure, header/payload JSON check (no signature verification)
Hex Color HexColor(value) #RGB, #RGBA, #RRGGBB, #RRGGBBAA
Lat/Lon LatLon(lat, lon) Decimal degree range validation
Belgian National Number BelgianNationalNumber(value) 11-digit Rijksregisternummer with MOD-97 check
Dutch BSN DutchBSN(value) 9-digit Burgerservicenummer with 11-check
Constraint Validators

Validators that enforce business constraints on values rather than format.

Validator Function Description
Date In Past DateInPast(value, maxAge) Date must be in the past, optionally within a max duration
Date In Future DateInFuture(value, maxAhead) Date must be in the future, optionally within a max duration
Date Range DateRange(value, min, max) Date must fall between min and max (inclusive, either bound optional)
Age At Least AgeAtLeast(birthDate, minYears) Birth date must represent at least N years of age
Number In Range NumberInRange(value, min, max) Exact decimal comparison via math/big.Rat — no float errors
Number In Range (float) NumberInRangeFloat(value, min, max) Float64 convenience variant (caller accepts precision tradeoffs)
String Length StringLength(value, min, max) Unicode rune count within bounds (multi-byte safe)

Structured Errors

Every validator returns a Result:

type Result struct {
    Valid    bool              `json:"valid"`
    Errors   []ValidationError `json:"errors,omitempty"`
    Metadata map[string]any    `json:"metadata,omitempty"`
}

type ValidationError struct {
    Code    string         `json:"code"`              // Stable machine-readable identifier
    Message string         `json:"message"`            // Human-readable explanation
    Field   string         `json:"field,omitempty"`    // Which input field failed
    Context map[string]any `json:"context,omitempty"` // Structured key-value detail
}
Who uses what
Consumer Uses Ignores
Human / UI Message Code, Context
AI Agent Code, Context Message
Logging Code for grouping, Context for detail
Tests Code for assertions Message
Error codes

Codes follow a {validator}.{aspect}.{problem} convention and are defined as exported constants:

validators.ErrIBANChecksumInvalid  // "iban.checksum.invalid"
validators.ErrEmailFormatInvalid   // "email.format.invalid"
validators.ErrCreditCardChecksumInvalid // "creditcard.checksum.invalid"

Full list in result.go.

Context maps

Context provides the structured data an AI agent (or any programmatic consumer) needs to act on an error without parsing the message:

r := validators.IBAN("DE893704004405320130")
// r.Errors[0].Context:
// {
//   "value":           "DE893704004405320130",
//   "country_code":    "DE",
//   "expected_length": 22,
//   "actual_length":   20
// }
Metadata

On success, Metadata carries parsed information extracted during validation:

r := validators.CreditCard("4111111111111111")
// r.Metadata: {"network": "visa", "length": 16}

r = validators.Email("user@example.com", false)
// r.Metadata: {"domain": "example.com"}

r = validators.SWIFT("DEUTDEFF500")
// r.Metadata: {"bank_code": "DEUT", "country_code": "DE", "location": "FF"}

Empty values

All validators treat empty strings as valid. Use your framework's required-field check separately — validation and presence are different concerns.

Usage examples

Basic validation
r := validators.Email("user@example.com", false)
if r.Valid {
    fmt.Println("Domain:", r.Metadata["domain"])
}
With MX check
r := validators.Email("user@example.com", true)
if !r.Valid {
    fmt.Println(r.Errors[0].Message)
}
AI agent error handling
r := validators.Phone("+321234")
if !r.Valid {
    err := r.Errors[0]
    switch err.Code {
    case validators.ErrPhoneTooShort:
        // err.Context["digits"] has the actual count
    case validators.ErrPhoneCountryInvalid:
        // err.Context["country_code"], ["expected_min"], ["expected_max"]
    case validators.ErrPhoneFormatInvalid:
        // missing + prefix
    }
}
Constraint validators
// Age gate — exact calendar year calculation
r := validators.AgeAtLeast("2010-06-15", 18)
// r.Errors[0].Code: "ageatleast.range.too_young"
// r.Errors[0].Context: {"age": 15, "min_age": 18, ...}

// Financial amount — exact decimal, no float drift
r = validators.NumberInRange("19.99", "0.01", "9999.99")

// Date must be in the past, at most 10 years ago
r = validators.DateInPast("2020-01-01", 10 * 365 * 24 * time.Hour)

// String length in Unicode runes (not bytes)
r = validators.StringLength("héllo 🌍", 1, 10) // length = 8 runes
JSON serialization

The Result struct serializes directly to JSON — no adapter needed:

r := validators.IBAN("INVALID")
data, _ := json.Marshal(r)
// {
//   "valid": false,
//   "errors": [{
//     "code": "iban.length.too_short",
//     "message": "IBAN is too short",
//     "field": "iban",
//     "context": {"value": "INVALID", "length": 7}
//   }]
// }

Architecture decisions

See docs/adr/ for the reasoning behind:

License

MIT

Documentation

Index

Constants

View Source
const (
	ErrEmailFormatInvalid = "email.format.invalid"
	ErrEmailDomainNoMX    = "email.domain.no_mx"
)

Error codes — email

View Source
const (
	ErrIBANTooShort        = "iban.length.too_short"
	ErrIBANInvalidChars    = "iban.characters.invalid"
	ErrIBANCountryInvalid  = "iban.country.invalid"
	ErrIBANLengthMismatch  = "iban.length.mismatch"
	ErrIBANChecksumInvalid = "iban.checksum.invalid"
)

Error codes — IBAN

View Source
const (
	ErrSWIFTLengthInvalid = "swift.length.invalid"
	ErrSWIFTFormatInvalid = "swift.format.invalid"
)

Error codes — SWIFT/BIC

View Source
const (
	ErrPhoneTooShort       = "phone.length.too_short"
	ErrPhoneTooLong        = "phone.length.too_long"
	ErrPhoneInvalidChars   = "phone.characters.invalid"
	ErrPhoneFormatInvalid  = "phone.format.invalid"
	ErrPhoneCountryInvalid = "phone.country.invalid"
)

Error codes — phone

View Source
const (
	ErrCreditCardTooShort        = "creditcard.length.too_short"
	ErrCreditCardTooLong         = "creditcard.length.too_long"
	ErrCreditCardInvalidChars    = "creditcard.characters.invalid"
	ErrCreditCardChecksumInvalid = "creditcard.checksum.invalid"
)

Error codes — credit card

View Source
const (
	ErrVATTooShort       = "vat.length.too_short"
	ErrVATCountryInvalid = "vat.country.invalid"
	ErrVATFormatInvalid  = "vat.format.invalid"
)

Error codes — VAT

View Source
const (
	ErrURLFormatInvalid = "url.format.invalid"
	ErrURLSchemeInvalid = "url.scheme.invalid"
	ErrURLHostMissing   = "url.host.missing"
)

Error codes — URL

View Source
const (
	ErrUUIDFormatInvalid  = "uuid.format.invalid"
	ErrUUIDVersionInvalid = "uuid.version.invalid"
)

Error codes — UUID

View Source
const (
	ErrPostalCodeCountryInvalid = "postalcode.country.invalid"
	ErrPostalCodeFormatInvalid  = "postalcode.format.invalid"
)

Error codes — postal code

View Source
const (
	ErrCountryCodeFormatInvalid = "countrycode.format.invalid"
	ErrCountryCodeUnknown       = "countrycode.unknown"
)

Error codes — country code

View Source
const (
	ErrCurrencyCodeFormatInvalid = "currencycode.format.invalid"
	ErrCurrencyCodeUnknown       = "currencycode.unknown"
)

Error codes — currency code

View Source
const (
	ErrDateFormatInvalid = "date.format.invalid"
	ErrDateInvalid       = "date.value.invalid"
)

Error codes — date

View Source
const (
	ErrIPv4FormatInvalid = "ipv4.format.invalid"
	ErrIPv4OctetInvalid  = "ipv4.octet.invalid"
)

Error codes — IPv4

View Source
const (
	ErrDomainFormatInvalid = "domain.format.invalid"
	ErrDomainLabelInvalid  = "domain.label.invalid"
	ErrDomainTooLong       = "domain.length.too_long"
)

Error codes — domain

View Source
const (
	ErrLEILengthInvalid   = "lei.length.invalid"
	ErrLEIFormatInvalid   = "lei.format.invalid"
	ErrLEIChecksumInvalid = "lei.checksum.invalid"
)

Error codes — LEI

View Source
const (
	ErrCIDRFormatInvalid = "cidr.format.invalid"
	ErrCIDRPrefixInvalid = "cidr.prefix.invalid"
)

Error codes — CIDR

View Source
const (
	ErrISBNFormatInvalid   = "isbn.format.invalid"
	ErrISBNChecksumInvalid = "isbn.checksum.invalid"
)

Error codes — ISBN

View Source
const (
	ErrCRONFormatInvalid = "cron.format.invalid"
	ErrCRONFieldInvalid  = "cron.field.invalid"
)

Error codes — CRON

View Source
const (
	ErrJWTFormatInvalid  = "jwt.format.invalid"
	ErrJWTSegmentInvalid = "jwt.segment.invalid"
)

Error codes — JWT

View Source
const (
	ErrLatLonFormatInvalid = "latlon.format.invalid"
	ErrLatitudeOutOfRange  = "latlon.latitude.out_of_range"
	ErrLongitudeOutOfRange = "latlon.longitude.out_of_range"
)

Error codes — latitude/longitude

View Source
const (
	ErrBNNLengthInvalid   = "bnn.length.invalid"
	ErrBNNFormatInvalid   = "bnn.format.invalid"
	ErrBNNDateInvalid     = "bnn.date.invalid"
	ErrBNNChecksumInvalid = "bnn.checksum.invalid"
)

Error codes — Belgian National Number

View Source
const (
	ErrBSNLengthInvalid   = "bsn.length.invalid"
	ErrBSNFormatInvalid   = "bsn.format.invalid"
	ErrBSNChecksumInvalid = "bsn.checksum.invalid"
)

Error codes — Dutch BSN

View Source
const (
	ErrDateInPastNotPast = "dateinpast.range.not_past"
	ErrDateInPastTooFar  = "dateinpast.range.too_far"
)

Error codes — date in past

View Source
const (
	ErrDateInFutureNotFuture = "dateinfuture.range.not_future"
	ErrDateInFutureTooFar    = "dateinfuture.range.too_far"
)

Error codes — date in future

View Source
const (
	ErrDateRangeBeforeMin = "daterange.range.before_min"
	ErrDateRangeAfterMax  = "daterange.range.after_max"
)

Error codes — date range

View Source
const (
	ErrNumberFormatInvalid = "number.format.invalid"
	ErrNumberBelowMin      = "number.range.below_min"
	ErrNumberAboveMax      = "number.range.above_max"
)

Error codes — number in range

View Source
const (
	ErrStringTooShort = "string.length.too_short"
	ErrStringTooLong  = "string.length.too_long"
)

Error codes — string length

View Source
const (
	ErrAgeAtLeastTooYoung = "ageatleast.range.too_young"
)

Error codes — age at least

View Source
const (
	ErrHexColorFormatInvalid = "hexcolor.format.invalid"
)

Error codes — hex color

View Source
const (
	ErrIPv6FormatInvalid = "ipv6.format.invalid"
)

Error codes — IPv6

View Source
const (
	ErrMACFormatInvalid = "mac.format.invalid"
)

Error codes — MAC address

View Source
const (
	ErrSemVerFormatInvalid = "semver.format.invalid"
)

Error codes — semver

Variables

This section is empty.

Functions

This section is empty.

Types

type Result

type Result struct {
	Valid    bool              `json:"valid"`
	Errors   []ValidationError `json:"errors,omitempty"`
	Metadata map[string]any    `json:"metadata,omitempty"`
}

Result is the unified return type for all validators.

func AgeAtLeast added in v1.1.0

func AgeAtLeast(birthDate string, minYears int) Result

AgeAtLeast validates that a birth date (YYYY-MM-DD) represents an age of at least minYears. Uses calendar year calculation (not duration), handling leap years correctly.

func BelgianNationalNumber

func BelgianNationalNumber(value string) Result

BelgianNationalNumber validates a Belgian Rijksregisternummer (11 digits). Format: YY.MM.DD-SSS.CC where SSS is a sequence number and CC is a MOD-97 check.

func CIDR

func CIDR(value string) Result

CIDR validates an IP address in CIDR notation (e.g., "10.0.0.0/8", "fd00::/64").

func CRON

func CRON(value string) Result

CRON validates a 5-field cron expression (minute hour day month weekday).

func CountryCode

func CountryCode(value string) Result

CountryCode validates an ISO 3166-1 country code (alpha-2 or alpha-3).

func CreditCard

func CreditCard(value string) Result

CreditCard validates a credit card number using the Luhn algorithm. Detects the card network from the IIN prefix.

func CurrencyCode

func CurrencyCode(value string) Result

CurrencyCode validates an ISO 4217 currency code.

func Date

func Date(value string) Result

Date validates an ISO 8601 date string (YYYY-MM-DD).

func DateInFuture added in v1.1.0

func DateInFuture(value string, maxAhead time.Duration) Result

DateInFuture validates that a date string (YYYY-MM-DD) is in the future. If maxAhead is > 0, the date must not be further ahead than maxAhead from today. A maxAhead of 0 means any future date is accepted.

func DateInPast added in v1.1.0

func DateInPast(value string, maxAge time.Duration) Result

DateInPast validates that a date string (YYYY-MM-DD) is in the past. If maxAge is > 0, the date must not be further back than maxAge from today. A maxAge of 0 means any past date is accepted.

func DateRange added in v1.1.0

func DateRange(value, minDate, maxDate string) Result

DateRange validates that a date string (YYYY-MM-DD) falls between min and max (inclusive). Either min or max may be empty to leave that bound open.

func Domain

func Domain(value string) Result

Domain validates a domain name per RFC 1035.

func DutchBSN

func DutchBSN(value string) Result

DutchBSN validates a Dutch Burgerservicenummer (9 digits, 11-check).

func Email

func Email(value string, checkMX bool) Result

Email validates an email address. If checkMX is true, it verifies the domain has MX records.

func HexColor

func HexColor(value string) Result

HexColor validates a CSS hex color code (#RGB, #RGBA, #RRGGBB, or #RRGGBBAA).

func IBAN

func IBAN(value string) Result

IBAN validates an International Bank Account Number.

func IPv4

func IPv4(value string) Result

IPv4 validates an IPv4 address in dotted-decimal notation.

func IPv6

func IPv6(value string) Result

IPv6 validates an IPv6 address.

func ISBN

func ISBN(value string) Result

ISBN validates an ISBN-10 or ISBN-13.

func JWT

func JWT(value string) Result

JWT validates the structure of a JSON Web Token (three base64url-encoded segments). It does NOT verify the cryptographic signature.

func LEI

func LEI(value string) Result

LEI validates a Legal Entity Identifier (ISO 17442). 20 alphanumeric characters with a MOD-97 check.

func LatLon

func LatLon(lat, lon float64) Result

LatLon validates a geographic coordinate pair (latitude, longitude) in decimal degrees.

func MAC

func MAC(value string) Result

MAC validates a 48-bit MAC address in colon, hyphen, or dot notation.

func NumberInRange added in v1.1.0

func NumberInRange(value, min, max string) Result

NumberInRange validates that a decimal number string falls within [min, max]. Uses exact decimal arithmetic (math/big.Rat) — no floating point errors. Either min or max may be empty to leave that bound open.

func NumberInRangeFloat added in v1.1.0

func NumberInRangeFloat(value, min, max float64) Result

NumberInRangeFloat validates that a float64 falls within [min, max]. Convenience wrapper for callers who already have numeric types. Uses floating point comparison — caller accepts precision tradeoffs.

func Phone

func Phone(value string) Result

Phone validates an international phone number in E.164-like format. Expects a leading + followed by country code and subscriber number.

func PostalCode

func PostalCode(value, countryCode string) Result

PostalCode validates a postal/ZIP code for a given ISO 3166-1 alpha-2 country code.

func SWIFT

func SWIFT(value string) Result

SWIFT validates a SWIFT/BIC code.

func SemVer

func SemVer(value string) Result

SemVer validates a semantic version string per semver.org.

func StringLength added in v1.1.0

func StringLength(value string, min, max int) Result

StringLength validates that a string's length in Unicode rune count falls within [min, max]. Uses rune count, not byte count, so multi-byte characters are counted correctly. Set min to 0 to skip minimum check. Set max to 0 to skip maximum check.

func URL

func URL(value string) Result

URL validates a URL string. Requires a scheme (http, https, ftp, ftps) and a host.

func UUID

func UUID(value string, version int) Result

UUID validates a UUID string. If version > 0, also checks the version nibble.

func VAT

func VAT(value string) Result

VAT validates a European VAT identification number.

type ValidationError

type ValidationError struct {
	Code    string         `json:"code"`
	Message string         `json:"message"`
	Field   string         `json:"field,omitempty"`
	Context map[string]any `json:"context,omitempty"`
}

ValidationError carries a machine-readable code, a human-readable message, and structured context so both humans and AI agents can act on failures.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL