[ AxiomCore · Documentation ]
Validation
Field constraints, type constraints, shared validators and codecs, domain invariants and their limits in Acore.
Acore validates data at several layers. They are related but not interchangeable: a rule at one layer does not enforce anything at another. This page lists each layer, its syntax and its limits. Backend examples were checked with Axiom CLI 0.149.1.
| Layer | Purpose | Enforced by |
|---|---|---|
| General type and constraint | Validate authored values during evaluation | Evaluator |
| Model field constraint | Keep source-schema rules such as length and range | Contract build and supported runtime paths |
| Shared validator and codec | Cross-field checks and exact representations | Backend admission, forms and request adapters |
Endpoint validate | Enable or override request and response validation intent | The selected runtime path |
| Domain invariant | Business meaning for review and impact analysis | Signed manifest and tooling; not executed against data |
| Security rule | Report or gate coverage and unsafe combinations | Analyzer, build and release, by mode |
Model field constraints
Write constraints as named arguments on the field type.
project(id: "field-rules", version: "1.0.0")
model User {
email: String(minLength: 3, maxLength: 254, format: "email")
code: String(pattern: "^[A-Z]{3}$")
age: Int32(min: 0, max: 150)
status: Field(validate: {type: "enum", values: ["open", "done"]}, type: String)
}| Type | Arguments | Rules |
|---|---|---|
String | minLength, maxLength | Nonnegative integers |
String | pattern | A valid regular expression |
String | format | A string, such as "email" |
Int32, Int64, Float32, Float64 | min, max | Finite numbers |
| Any | Field(validate: {...}, type: T) | A structured rule object. Use it for enumerations; String(values: [...]) is rejected. |
A minimum greater than its maximum is rejected
(Minimum constraint must not exceed maximum), and an argument from another
type is rejected (Unknown argument 'minLength'; expected min, max).
Extractors produce the same structured rules from supported framework
metadata. The contract stores them on the field's validate value. A field
with no rules has no validate value; it never serializes as an empty object.
Type constraints in general modules
sampleRate: Float(this >= 0.0 && this <= 1.0) = 0.25The evaluator checks the constraint when the property is evaluated. These constraints validate configuration values, not request payloads.
Domain invariants
project(id: "invariant-example", version: "1.0.0")
model Order { id: String total_cents: Int64 } also entity
invariant nonNegativeTotal(
scope: Entities.Order,
expression: "total_cents >= 0",
fields: ["total_cents"]
)An invariant must scope an entity through Entities.<name>. Its expression and
rule objects are signed evidence for review, generation and impact analysis.
The compiler does not run business expressions against production data. See
Domain and security models.
Failure behavior
Invalid authored types or structural references stop the build. At runtime, a payload failure is reported through the selected SDK or runtime error state. The exact error transport differs by target, so test your consuming framework and do not assume one UI error shape across all clients.
Shared validators and codecs
Experimental Shared validators and codecs are part of the managed language foundations.
A backend module opts into the axiom-server-plan/v2 plan format when it
declares pure functions, validators, codecs or explicit model defaults. Older
plans keep their version and validation behavior. Public language metadata
requires reader version 2.
project(id: "schema-example", version: "1")
model Window {
title: String(minLength: 1, maxLength: 200)
start: Int32(min: 0)
end: Int32(min: 0)
count: Int32 = 4
note: String?
}
pure fn normalizeTitle(value: String): String = value.trim()
validator dates(input: Models.Window) {
check(condition: Input.end >= Input.start, field: .end,
code: "end_before_start", message: "End must follow start")
}
validation(model: Models.Window, rules: [Validators.dates], expose: true)
codec taskId(encoded: String, decoded: Int64,
decode: Functions.parseCanonicalInt64,
encode: Functions.formatCanonicalInt64, expose: true)| Declaration | Signature |
|---|---|
validator | validator name(input: TypeRef) { check(...) } |
check | check(condition: PureExpression, field: .field, code: String, message: String) |
validation | validation(model: TypeRef, rules: List<Validator>, expose: Bool?) |
codec | codec name(encoded: TypeRef, decoded: TypeRef, decode: PureFunction, encode: PureFunction, expose: Bool?) |
Defaults and null
- A default applies only when the field is absent. An explicit
nullis a supplied value and needs a nullable type:String?allows absence and null. - Defaults are checked against the field's type and constraints. An invalid
default fails with
invalid schema default.
Issues
- Validation returns ordered issues. Each has a path (field, list index, or a hashed map key), a stable code, a message key, a safe fallback message and the rule identity. An issue never contains the rejected value.
- Independent field errors accumulate in field order. Cross-field rules wait until the fields they depend on have valid types.
- Truncating the issue list makes the report invalid; it never counts as a successful validation.
Limits
| Limit | Value |
|---|---|
| Work | 10,000 units |
| Nesting depth | 32 |
| Input size | 1 MiB |
| Issues | 64 issues or 32 KiB |
Pure expressions cannot access providers, application state, time, randomness or extensions. Recursion and unbounded loops are rejected. Validation is separate from authorization and from asynchronous availability checks. The backend re-checks input, output and provider values. Extension guests use the selected helpers, and the broker still checks guest values and proposals independently.
Endpoint bodies and projections
A native endpoint can bind a named model body and expose an approved projection of it:
project(id: "checked-window", version: "1")
variant default(include: ["*"])
model Window {
title: String(minLength: 1, maxLength: 200)
start: Int32(min: 0)
end: Int32(min: 0)
}
entity window(model: Models.Window, key: ["start"])
projection windowInput(entity: Entities.window,
fields: [.title, .start, .end], audience: Audience.web)
validator dates(input: Models.Window) {
check(condition: Input.end >= Input.start, field: .end,
code: "end_before_start", message: "End must follow start")
}
validation(model: Models.Window, rules: [Validators.dates], expose: true)
model CreateWindow { value: Models.Window }
action createWindow(kind: ActionKind.command,
input: Models.CreateWindow, output: Models.Window) {
managed { return Input.value }
}
endpoint createWindow(method: POST, path: "/windows") {
request(projection: Projections.windowInput) {
body("value", type: Models.Window)
}
handle(action: Actions.createWindow, input: {value: Input.body.value})
response(type: Models.Window)
}The owning AxiomDeps.toml declares audiences = ["web"]. The HTTP body is
{"title":"Review","start":1,"end":2}. The named body parameter supplies the
action's value, and the selected frontend arguments are title, start and
end. Invalid fields accumulate before the action runs.
- Rules and codecs are private by default.
expose: truepublishes their metadata. Frontend operation locks then select the authorized request fields and function closure, so a rule that needs unselected input cannot leak into a client. - Declare
request(projection: Projections.name)with the input model's approved projection. The native backend uses one named model body parameter; its HTTP body and form fields stay flat. The selected descriptor carries the projection's field constraints and exposed cross-field rules. Older flat-parameter projection contracts use the same selected rules. - Separate scalar body parameters without a projection carry only their declared types. They do not inherit an action's private input model or rules.
- Forms and request adapters apply absent defaults and reject malformed input before dispatch. Backend admission remains authoritative.
Exact numbers, dates and bytes
Backend Int64 codecs keep exact signed 64-bit values. Portable helpers reject
integers outside ±9,007,199,254,740,991 and never round an identifier into a
JavaScript number. Use canonical string codecs for larger integers, arbitrary
precision integers or decimals, dates, UTC timestamps and base64 bytes.
| Pure function | Signature |
|---|---|
parseCanonicalInt64 | (value: String): Int64 |
formatCanonicalInt64 | (value: Int64): String |
canonicalBigInt, canonicalDecimal, canonicalDate, canonicalTimestamp, canonicalBase64 | (value: String): String; reject noncanonical input |
- A canonical decimal string has a decimal point and no trailing fractional
zeros, except the single
.0form. - A canonical UTC timestamp fraction has 3, 6 or 9 digits with no redundant final zero group.
- A codec's
encodeanddecodeare type-checked and must round-trip.
Portable rule limits
Portable selection rejects IP and date rules, recursive rule expansion, and regular expressions outside the bounded subset before activation. The portable regex subset supports literals, character classes and anchors with at most one repetition; groups, alternatives and engine-specific escapes are rejected. Recursive models stay bounded in the shared backend engine. The UI profile's own type rules still decide whether a model can be selected.
The Rust guest SDK exposes the shared engine through its schema feature, and
the TypeScript guest SDK through @axiomcore/extension-sdk/schema. Neither
helper grants an effect or bypasses a guard.
Related
Imports, amends and variants
Choose the Acore keyword that matches the ownership and trust boundary, compose backend sources with amends, and select variants.
Managed logic and concurrency
Write bounded business logic in managed actions, declare typed failures, private setup, transactions, parallel groups, operation and client policies, and durable jobs.