Validation declarations#
Business Language has three validation surfaces:
| Surface | Use it for |
|---|---|
validation Name { ... } | Reusable validation groups and helper functions |
validation rule Name { ... } | Standalone rule blocks with required failure text |
Inline validation { ... } sections | Field, table, entity, and class-local checks |
Validation should describe structural correctness and local invariants. When a failure depends on required configuration, resolve that configuration in a function or service and raise a declared message if the row is missing or ambiguous.
Named validation blocks#
A named validation block can contain ensure, check, and require statements. In this form, the message, on, and else trailers are optional.
<!-- bl-example id="validation-named-block" entry-rule="validationDeclaration" -->
validation PostingPolicyShape {
ensure active == true;
check company_code is not null on company_code;
require document_type is not null else "Document type is required";
}Use named blocks when the same validation logic is applied from more than one table, form, service, or test.
Validation helper functions#
Named validation blocks can also declare validate helper functions. The return type can use either : or ->.
<!-- bl-example id="validation-helper-functions" entry-rule="validationDeclaration" -->
validation VendorPayloadValidation {
validate has_vendor_scope(vendor_id: VendorId, company_code: CompanyCode): bool {
return vendor_id is not null && company_code is not null;
}
validate normalized_name(input: string, fallback: string = "n/a",) -> string {
return input ?? fallback;
}
}Default parameters and trailing commas follow the same function parameter grammar used by normal functions.
Standalone validation rules#
Standalone validation rule declarations are stricter than named validation blocks. ensure requires a message, check requires an on target, and require requires an else string.
<!-- bl-example id="validation-rule-required-trailers" entry-rule="validationRuleDeclaration" -->
validation rule RequiredInvoiceAmount {
ensure amount is not null message "Amount is required";
check amount > 0 on amount;
require currency_code is not null else "Currency is required";
}Use these for compact reusable checks. Do not use inline text as a substitute for declared message catalogs at service or command boundaries.
Inline field validation#
Field declarations can keep scalar validation close to the field type.
<!-- bl-example id="field-inline-validation" entry-rule="fieldDeclaration" -->
field VendorCode: string {
max_length: 40;
validation {
not_blank: !(value is null or empty);
configured_format: rule VendorCodeFormat(value);
};
}Inline field rules are named entries. They can be expressions or references to reusable validation rule declarations.
Inline table validation#
Tables can declare validation sections alongside fields, constraints, indexes, UI blocks, and table-local functions.
<!-- bl-example id="table-inline-validation" entry-rule="tableDeclaration" -->
table PostingPolicy {
field company_code: CompanyCode key;
field document_type: DocumentType key;
field active: bool required;
field valid_from: date required;
field valid_to: date;
validation {
valid_dates: valid_to is null || valid_to >= valid_from;
active_has_document_type: document_type is not null;
}
}Keep table validation deterministic. A missing policy row is not a validation success and should not become an implicit default.
Closed failure boundary#
Validation statements can reject malformed input, inconsistent local state, and invariant violations. They should not decide tenant-specific approval routes, status outcomes, reason requirements, thresholds, or routing weights by hardcoded branches.
For configurable policy:
- Store the policy in tables.
- Resolve the exact row in a function or service.
- Raise a declared message when required configuration is missing or ambiguous.
- Cover the success and failure paths in tests.