Validation
Validation is Paradox’s system for expressing invariants on types. Validation rules are refinement types — constraints that restrict the set of valid inhabitants of a type. Paradox verifies these constraints at compile time using SMT solving (Z3) and generates runtime validators in every target language.
Product Validation
Add constraints to product types with valid:
type Pet
name: Text
age: Integer
species: Species
valid Pet
name != "" | Pet's gotta have a name
age > 0 | No such thing as a negative age for a pet
Each line in the valid block is a boolean expression. If the expression is followed by |, the text after the pipe is a custom error message.
Multiple constraints are combined with logical AND — all must hold simultaneously.
Union Validation
Validate specific tags of a union type:
union Status
approved: Integer
rejected
pending
valid Status
approved: amount. amount > 0 | approved amount must be positive
rejected: true
pending: true
Each tag gets its own validation rule. Tags with payloads bind a variable for use in the constraint.
Wrap Validation
Validate the inner value of a wrap type using unwrap:
wrap Natural: Integer
valid Natural
unwrap >= 0
wrap Positive: Natural
valid Positive
unwrap as Integer != 0
wrap Port: Natural
valid Port
unwrap as Integer <= 65535
Wrap validation is the foundation of refinement types in Paradox. The unwrap keyword accesses the inner value, and as casts through the wrap chain.
Match in Validation
Use pattern matching inside validation for field-dependent constraints:
valid Pet
name != "" | Pet's must have a name
age > 0 | No such thing as a negative age for a pet
match address.country:
usa: species = Species.cat
_: true
match species:
cat: age > 1
horse: (name as Text) = "tonto"
dog: true
Match expressions inside valid blocks dispatch on field values, allowing different constraints for different cases.
Implication
The -> operator expresses conditional constraints:
valid Pet
name = "tonto" -> age > 13
This means: "if the name is tonto, then age must be greater than 13." Implication is equivalent to not a || b — the constraint only applies when the antecedent is true.
Error Messages
Custom error messages follow the | pipe after the constraint expression:
valid User
first != ""
last != ""
age > 18 | Must be an adult
If no error message is provided, a default message is generated from the expression itself.
SMT Verification
Paradox uses Z3 (an SMT solver) to verify validation rules at compile time. This catches two categories of errors:
Overconstrained Types
If constraints are logically contradictory, the type is uninhabitable:
type BadRectangle
width: Integer
height: Integer
area: Integer
valid BadRectangle
width > 0
height > 0
area = width * height
area < 0
[error 22]: Overconstrained validation for BadRectangle
12 │ valid BadRectangle
13 │ width > 0
14 │ height > 0
15 │ area = width * height
16 │ area < 0
│ Note: These constraints make constructing BadRectangle impossible
The SMT solver proves that no values can satisfy all constraints simultaneously.
Refinement Violations
When constructing a refined type, the value must provably satisfy the constraint:
wrap PositiveInt: Integer
valid PositiveInt
unwrap > 0
badZero: PositiveInt
PositiveInt 0
[error 40]: Refinement type constraint violated
7 │ unwrap > 0 | Must be positive
• ╰╸ valid PositiveInt is defined here
15 │ PositiveInt 0
• ╰╸ this value does not satisfy the constraint: unwrap > 0
│ Hint: The value must satisfy: unwrap > 0
This also catches more subtle violations, like arithmetic that might produce invalid values:
wrap StockCount: Integer
valid StockCount
unwrap >= 0
decrementStock: current:StockCount. quantity:Integer. StockCount
StockCount (current.unwrap - quantity)
The SMT solver recognizes that current.unwrap - quantity might be negative, violating the unwrap >= 0 constraint.
Generated Validators
For every type with validation, Paradox generates runtime validator functions in every target language. For example, given:
type Pet
name: Text
age: Integer
valid Pet
name != "" | Pet's gotta have a name
age > 0
The TypeScript output includes:
export const validPet = (input: unknown): Pet => {
const _x = input as globalThis.Record<string, unknown>;
// ... field type checking ...
const x = input as Pet;
const errors: string[] = [];
if (x.name === "") errors.push(`Pet's gotta have a name`);
if (x.age <= 0) errors.push(`Validation failed: age > 0`);
if (errors.length > 0) throw new Error(errors.join('; '));
return x;
}
And the Haskell output:
validPet :: Pet -> Either Text Pet
validPet x =
let errors = catMaybes
[ if x.name == "" then Just "Pet's gotta have a name" else Nothing
, if x.age <= 0 then Just "Validation failed: age > 0" else Nothing
]
in if null errors then Right x else Left (T.intercalate "; " errors)
See Also
-
Types — The types that validation constrains
-
Illumination — Display logic (the other interface)
-
SMT Refinements — Deep dive on Z3 verification