Acore language

[ 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.

LayerPurposeEnforced by
General type and constraintValidate authored values during evaluationEvaluator
Model field constraintKeep source-schema rules such as length and rangeContract build and supported runtime paths
Shared validator and codecCross-field checks and exact representationsBackend admission, forms and request adapters
Endpoint validateEnable or override request and response validation intentThe selected runtime path
Domain invariantBusiness meaning for review and impact analysisSigned manifest and tooling; not executed against data
Security ruleReport or gate coverage and unsafe combinationsAnalyzer, build and release, by mode

Model field constraints

Write constraints as named arguments on the field type.

backend/main.acore
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)
}
TypeArgumentsRules
StringminLength, maxLengthNonnegative integers
StringpatternA valid regular expression
StringformatA string, such as "email"
Int32, Int64, Float32, Float64min, maxFinite numbers
AnyField(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

Acore excerpt
sampleRate: Float(this >= 0.0 && this <= 1.0) = 0.25

The evaluator checks the constraint when the property is evaluated. These constraints validate configuration values, not request payloads.

Domain invariants

backend/main.acore
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.

backend/main.acore
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)
DeclarationSignature
validatorvalidator name(input: TypeRef) { check(...) }
checkcheck(condition: PureExpression, field: .field, code: String, message: String)
validationvalidation(model: TypeRef, rules: List<Validator>, expose: Bool?)
codeccodec 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 null is 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

LimitValue
Work10,000 units
Nesting depth32
Input size1 MiB
Issues64 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:

backend/main.acore
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: true publishes 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 functionSignature
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 .0 form.
  • A canonical UTC timestamp fraction has 3, 6 or 9 digits with no redundant final zero group.
  • A codec's encode and decode are 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.