How does EK9 handle input validation?
← Security and Sanitization · Ref: Q269
EK9 provides four validation mechanisms that work together: constrained types for structural validation, declaration guards for conditional execution, require statements for preconditions, and sanitized parameters that clean external input on entry.
CONSTRAINED TYPES
Constrained types validate at construction time. A bare constructor ASSERTS the value is valid: a set value that violates the constraint panics at runtime (and a literal constant that provably violates is the compile error E08260), so use a bare constructor only for values you already trust:
EmailAddress as String constrain as matches /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ port <- Port(8080)
For untrusted or boundary input use the fallible .of(value) factory instead. It takes the base-type value and returns an UNSET object if the value violates the constraint (or is itself unset), and never panics.
DECLARATION GUARDS
Guards combine assignment with set-checking in one line:
if email <- EmailAddress().of(userInput) processEmail(email)
The block only executes if construction succeeds. This prevents processing of invalid data.
REQUIRE STATEMENTS
Require validates preconditions at the start of a function:
require input? require length input > 0
A failed require throws an exception, enforcing the contract.
SANITIZED PARAMETERS
The sanitized modifier makes the RECEIVING function clean the parameter the moment it is entered, at run time, on every route into it:
-> userInput as sanitized String
A detected threat (an injection, a script) arrives as an UNSET String and is logged. The compiler guarantees that cleaning step exists - the author cannot forget it - but it does not track the value afterwards.
FOUR LAYERS TOGETHER
The complete validation flow uses all four:
1. sanitized cleans the input on entry (a threat becomes unset) 2. Constrained type validates the format 3. Declaration guard checks construction succeeded 4. require enforces additional business rules
Each layer catches a different class of invalid input.
See Q215 for sanitized parameters. See Q257 for constrained types. See Q29 for unset variables and guards. See Q74 for declaration guards. See Q268 for OWASP vulnerability prevention. See Q272 for defense in depth.
Example
defines module qa.security.inputvalidation defines type // Constrained type: validates format at construction EmailAddress as String constrain as matches /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ // Constrained type: validates range Port as Integer constrain >= 1 and <= 65535 defines function // All four validation layers validateAndProcess() as pure -> input as sanitized String <- result as String: String() // Layer 1: 'sanitized' - this function cleans 'input' on entry, at run time; // a detected threat arrives as an unset String // Layer 2: the constrained type validates the format safeCopy <- String(input) // Layer 3: guard checks fallible construction succeeded if email <- EmailAddress().of(safeCopy) // Layer 4: require enforces business rules require length $email > 5 result: "Valid email: " + $email defines program InputValidationDemo() stdout <- Stdout() // === CONSTRAINED TYPE VALIDATION === if email <- EmailAddress().of("user@example.com") stdout.println("Valid: " + $email) //.of() returns an UNSET EmailAddress when the value violates the constraint badEmail <- EmailAddress().of("not-an-email") if not badEmail? stdout.println("Invalid email caught by constraint") // === PORT RANGE VALIDATION === if port <- Port().of(8080) stdout.println("Valid port: " + $port) badPort <- Port().of(99999) if not badPort? stdout.println("Invalid port caught by constraint") // === COMBINED VALIDATION === validResult <- validateAndProcess("user@example.com") if validResult? stdout.println(validResult) //A rejected input leaves the result unset invalidResult <- validateAndProcess("bad") if not invalidResult? stdout.println("Combined validation rejected invalid input")
Common mistakes
E08260 — A bare constructor asserts the value is valid. With a literal the compiler can prove violates the constraint (99999 > 65535) it is a compile error rather than a runtime panic. Use the fallible .of(value) factory for values you do not already trust. See ek9 -h E08260 for details.
Incorrect:
if port <- Port(99999)
Correct:
if port <- Port().of(8080)
E50010 — Only built-in types like String, Integer, and Float can be constrained. Custom records and classes cannot be constrained because they do not have the comparison semantics needed for constraint evaluation. See ek9 -h E50010 for details.
Incorrect:
ValidatedUser as UserRecord constrain as matches /^[a-zA-Z]+$/
Correct:
EmailAddress as String constrain as matches /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
Other ways to ask this
- What input validation mechanisms does EK9 provide?
- How do I validate user input in EK9?
- How do constrained types and guards work together for validation?
Coming from another language?
Java: Bean Validation annotations, manual null checks, OWASP ESAPI. Python: no compile-time validation, runtime libraries. Rust: newtype pattern with manual validation. Go: manual if-err checks. Kotlin: nullable types and require() function. EK9: four integrated mechanisms (constrained types, guards, require, sanitized) that compose into a validation pipeline.
Keywords: check, null-safe, sanitize, security, isset, verify, form, input, guard, require, constrain, validation, safe, protect, validate