Writing validation rules
Load this card when a task adds or changes a ValidationRules rule set, a
RangeMin or RangeMax bound (including a quoted min or max on the
numeric annotation) or a String Pattern. Its rules are generated from the
Data Validation Rules specification, which
owns them. Paths follow the
Libertas Schema Path
and its reference availability rules.
The ValidationRules quick reference lists the syntax and
the annotation inventory gives each annotation's Rust
and TypeScript spelling.
Some owners continue a quoted rule ending in a colon with a code block, such as the precedence table and the grammar; follow the owner link for it.
Order of work
- Decide where each check belongs (
validation.placement.*,validation.authoring.*) before writing any rule. - Choose the form: a rule set (
validation.declare.*,validation.syntax.*,validation.finalonly.*), a dynamic bound (validation.range.*) or a String format (validation.pattern.*,validation.regex.*,validation.escape.*,validation.literal.*). - Write each expression with typed values and operators (
validation.types.*,validation.null.*,validation.unanswered.*,validation.fallback.*,validation.boolean.*,validation.equality.*,validation.relational.*,validation.arithmetic.*,validation.precedence.*) and the functions it needs (validation.is.*,validation.calendar.*,validation.collection.*,validation.cursor.*,validation.collect.*,validation.unique.*,validation.count.*,validation.sum.*,validation.string.*,validation.resource.*,validation.matches.*,validation.bitflags.*). - Handle first-time composition without a reference context and every
nullable read (
validation.reference.*,validation.static.*). - Declare each message key in
StringResourcesunder the source documentation card, compile with Studio and fix every reported rule, then test the rules withevaluate_validation_rulesortest-rulesfixtures (validation.debug.*).
Rules to apply
Studio does not check these rules. Each is quoted from its owner.
1. Purpose
Owner: validation-rules.md#1-purpose
validation.purity.expression: A rule is a pure expression ofdataandreferencedData. During editing, any field MAY temporarily be undefined (unanswered), including aNullablefield whose Nullable choice has no answer yet. Undefined is local editor state, never encodable and never an answer;nullin a rule always means a present null, such as a Nullable's chosen null answer.validation.unanswered.aborts: A rule that reads an unanswered value is aborted: the client does not report its result and reports that value as a missing answer instead, in incremental and final validation alike. During editing the missing answer is the field's unanswered indicator, never a failure that rejects an incremental change; at final validation it blocks submission like any unanswered field. A rule's result therefore never depends on an unanswered value, and no rule distinguishes null from undefined.validation.null.fallback-not-answer-test(judgment): Within a rule,nullserves as a fallback value, chiefly through|, not as a test of whether the person answered: whether a field must be answered is structural, declared by a non-Nullablefield or aNullablefield without a Default.validation.null.accept-not-optional: Mandatory-field validation andValidationRulesare complementary, independent requirements. A rule MAY accept anull, but that success does not make the field optional or suppress the final mandatory-field check.validation.final.requirements: Final submission separately requires structural schema validity, every field answered (a non-Nullablefield present, aNullablefield null or present), and all validation rules to pass.validation.bytes.mime-empty-null: A zero-length MIME-aware Bytes value is a present, encodable Bytes field but has undefined media meaning and evaluates as null. Its non-Nullabledeclaration and size bounds do not require media selection; an App requiring a file declares a rule such as[] != null : "IMAGE_REQUIRED";.validation.bytes.lmb1-non-null: A defined LMB1 value evaluates as non-null, even when its content is empty. Ordinary Bytes keep their existing valuation.validation.placement.contexts: ValidationRules uses the shared schema reference contract. Rules may occur in editable Request/SubscriptionRequest data, inherited types, list items, and hidden/read-only fields. External reference reads require neither CopyFrom nor request-side copies of server metadata.validation.reference.first-composition: Authors MUST account for first-time request composition with no reference context: every curly reference evaluates tonullin that state. ValidationRules must handle that absence explicitly, for example with a null guard or a typed|fallback. The client still evaluates the rule normally; absence does not automatically make the rule pass.validation.reference.no-fetch: Neither validation nor request composition fetches server data to resolve these references.validation.valid.all-true: For the suppliedreferencedData, the data instance is valid iff every validation rule evaluates totrue.
2. Data Model
Owner: validation-rules.md#2-data-model
validation.types.semantic: Every successful intermediate evaluation carries an internal semantic type; operators MUST NOT infer a new type merely from the scalar's storage encoding. In particular, DateOnly is a calendar type even though its schema and wire encoding is the integeryyyyMMdd, and DateTime is an instant type even though its schema and wire encoding is integral Unix epoch seconds. Literals, paths, fallback results, operator results, and function results all retain or produce an explicit internal type.nullhas its own type.validation.types.date-literal-only: A DateOnly literal has type DateOnly; the language has no DateTime literal.validation.types.bitflags-host: A Bitflags host, an integer Number whoseBitflagsattribute names a flag catalog, is an ordinary integer in rules. Its catalog's members are schema constants that appear only as the right operand of the membership testhas.
3. Rule Set
Owner: validation-rules.md#3-rule-set
validation.declare.owners:ValidationRulesMAY be declared on:validation.declare.cumulative: A field/type rule is rooted at each concrete value of the annotated node. A named-type rule and a field-local rule are cumulative when a field references that named type. Inherited field rules are cumulative as well.validation.paths.root: The empty data path[]denotes the annotated value itself. Function rules address arguments by name, such as[minimum]and[options/timeout].validation.declare.spelling: The TypeScript source spelling is@LibertasValidationRules("..."). The Rust source spelling is#[libertas_validation_rules("...")].validation.declare.rust-helper: A Rust named type may use the standalone attribute; fields and variants use theLibertasExporthelper attribute. Parsers emit the raw string and source location. Libertas Studio's shared schema validator owns grammar and schema-decidable semantic validation.validation.finalonly.default: The default isfalse.validation.finalonly.incremental-tolerance(judgment): An omitted directive orFinalOnly=falseplaces the rule set in both incremental editor validation and final validation. Because an incremental failure restores the previous value, such a rule set MUST tolerate incomplete editing; an unanswered value never reaches a rule.validation.finalonly.true:FinalOnly=trueexplicitly omits the entire rule set from incremental validation and evaluates it only during final validation. This removes the need for that rule set to tolerate an initial incomplete editor state. It does not alternullexpression semantics, changeNullable, or replace final static schema checks.validation.finalonly.per-declaration: When field, inherited, and named-type rule sets accumulate, each declaration retains its ownFinalOnlyvalue.
Authoring guidance
Owner: validation-rules.md#authoring-guidance
validation.authoring.structural-first(judgment): Use the schema's precise type and ordinary structural attributes for nullability, numeric ranges, sizes, and uniqueness.ValidationRulesexpress semantic relationships that those declarations cannot express; they do not restate or replace structural constraints.validation.authoring.required-is-structural(judgment): When the person must answer a field, declare it structurally: a non-Nullablefield, or aNullablefield without a Default, whose Nullable choice starts unanswered. Do not restate that requirement as a!= nullrule; an unanswered value never reaches a rule.
Where a check belongs
Owner: validation-rules.md#where-a-check-belongs
-
validation.placement.save-path-authoritative(judgment): The App's save path is the authoritative validator. It checks every Request it receives in full, whether or not a client evaluated any rule. Client rules are a convenience for the person editing; they are never a security or correctness boundary, and never the only copy of a check. -
validation.placement.structural-checks(judgment):Check Where it belongs Type, nullability, numeric range, size, uniqueness, String format Schema declarations and Pattern. The client enforces them while editing, and the SDK's Avro decoder enforces each static bound (a number'smin,maxandstepincluded), and that every DateOnly value is a real Gregorian date, when the App decodes a Request, so App code and tests never re-check them. DynamicRangeMin/RangeMaxlimits stay save-path checks. -
validation.placement.local-feedback(judgment):Check Where it belongs Simple, local relationship where immediate feedback helps the person (one field, or fields on the same page) Optionally a rule set with the directive omitted (incremental and final), mirroring a save-path check. -
validation.placement.business-logic(judgment):Check Where it belongs Business logic: cross-page or cross-record consistency, legal or eligibility rules, anything needing App state The save path only. Reject with an Error response and name the field with ErrorFieldPath. -
validation.placement.final-only-discouraged(judgment): Final-only rule sets (an explicitFinalOnly=true;) are discouraged. They run at submission, the same moment as the save-path check, so they add no earlier feedback; they only add a second copy of the logic that can drift from the save path or fail silently. Put submission-time checks in the save path instead. Final-only rule sets remain valid and are still evaluated. Omitting the directive means the rule set is also evaluated incrementally; final-only must be written explicitly. -
validation.messages.split-for-presentation(judgment): The two forms above are validity-equivalent but intentionally not presentation-equivalent: the first form can display the key belonging to the selected failed rule, while the combined expression owns one message key.
Debugging rules
Owner: validation-rules.md#debugging-rules
validation.debug.trace: Studio offers an opt-in developer trace for each applicable rule. It records the expression text, message key, and source file and one-based line when known (otherwise null), and apassed,failed, orundecidedoutcome.undecidedmeans an evaluated read encountered an unanswered editor value; it is not a rule failure. Null or missing hosts and inactive Union payloads remain ordinary absence under the path rules, not unanswered values.validation.debug.fixtures: Each JSON fixture suppliesproject, optionalentryandphase,type,valuein schema JSON value form, andexpected, an object mapping message keys topassed,failed, orundecided. Relative project paths resolve from the fixture's directory. The runner reports every fixture and exits zero only when compilation and all expectations pass; invalid fixtures and mismatches exit nonzero.validation.debug.expectations: Every evaluated rule key requires an expectation, and unknown expected keys fail. For a key evaluated at several value occurrences, its outcome is failed if any occurrence fails, otherwise undecided if any is undecided, otherwise passed. A rule expected to fail is a successful test when it fails as expected. Standard output is one JSON report containing per-fixture results, diagnostics, traces and a summary.
3.1 Dynamic numeric ranges
Owner: validation-rules.md#31-dynamic-numeric-ranges
-
validation.range.purpose:RangeMinandRangeMaxreuse the Data Validation expression language to derive live inclusive constraints for a Number or DateTime value. -
validation.range.spelling: Their source spellings are@LibertasRangeMin("expression")/@LibertasRangeMax("expression")in TypeScript and#[libertas_range_min("expression")]/#[libertas_range_max("expression")]in Rust. The language-neutral schema stores the decoded expression string underRangeMinorRangeMax. -
validation.range.number-annotation: The numeric annotation also accepts a quoted expression inminormax:@LibertasNumber(type=i64, min=0, max="[^/maximum]")or#[libertas_number(min=0, max="[^/maximum]")]. Unquoted numeric literals and Rust$CONSTvalues continue to emitNumberMin/NumberMax; quoted expressions emitRangeMin/RangeMax, exactly like the standalone range annotations. Rust source constants inside expression text use the existing constant substitution.stepremains a fixed numeric value (or Rust source constant). No additional schema attributes are introduced. -
validation.range.context: Evaluation uses the same field-local context asValidationRules:[]is the annotated Number or DateTime and square paths traverse from it. The shared current-data path contract permits a leading parent prefix:[^/minimum]reads the current row'sminimum, and[^^/limits/maximum]reads a bound two levels above the field. -
validation.range.live-binding: Limits read current values whenever the normal editor refreshes and during final validation; rows bind independently, including after insertion, removal, reordering, or replacement. -
validation.range.reference-context: Curly paths inspect the current interaction reference context, including in editable requests. The result type must match the control's semantic type:Annotated control Required non-null result Ordinary Number, including TimeInterval Finite Number; integer and fractional bounds are permitted. DateOnly DateOnly calendar date. DateTime DateTime instant. -
validation.range.null-no-bound:nullmeans that no dynamic bound is currently available, including when the first request is composed before any reference context exists. It adds no constraint. -
validation.range.runtime-type-error: A non-null Boolean, String, array, collection, object, mismatched semantic type, or non-finite numeric result is an error. When an expression's result cannot be proved compatible statically, clients MUST check it at runtime and reject validation on an incompatible result. They MUST NOT silently remove the bound, erase its semantic type, or coerce the result to a Number. -
validation.range.expressions: Paths and arithmetic expressions that preserve the required type are supported; bounds are not restricted to literal constants. For example, a DateTime path plus an integer number of seconds remains a DateTime bound; subtracting two DateTime paths yields a Number and cannot bound DateTime. -
validation.range.inclusive: An activeRangeMinis an inclusive lower bound and an activeRangeMaxis an inclusive upper bound. -
validation.range.intersection: They intersect the native numeric type limits and any staticNumberMin/NumberMaxconstraints. Clients MUST apply that same intersection to final validation and to numeric, DateOnly, DateTime, and TimeInterval GUI controls. A changing dependency value MUST update the live GUI constraint. DateOnly bounds use theyyyyMMddwire representation at the schema boundary, but Data Validation evaluates the value as a real Gregorian calendar date. -
validation.range.datetime-backing: DateTime remains a distinct UI/wire and expression type but is constraint-backed as an unsigned integral Unix timestamp. Static Number constraints use that wire representation; dynamic Range expressions require a DateTime result. Their intersection applies to both its local date/time chooser and final UTC epoch-seconds validation. -
validation.range.step-unchanged:RangeMinandRangeMaxdo not change theNumberSteporigin or lattice. -
validation.range.unsatisfiable: If the active effective minimum exceeds the active effective maximum, the range is unsatisfiable; a client MUST NOT swap the two bounds or otherwise invent a valid interval. -
validation.range.absent-reference-null: A numeric field, including an editable request field, can use limits from the current reference context. When its reference objects do not provide them, these expressions return null and add no dynamic bound; ordinary numeric constraints still apply:TypeScript/** Measured value * A numeric result constrained by the available server-provided limits. * @LibertasNumber(type=f64) * @LibertasRangeMin('{$.limits.minimum}') * @LibertasRangeMax('{$.limits.maximum}') */ value: number; -
validation.range.no-fetch: These curly references can directly constrain editable request fields. The client reads the already received response; it does not fetch server state. The server remains responsible for enforcing its current authoritative limits.
4.4 Null Values
Owner: validation-rules.md#44-null-values
validation.null.equality: Every field uses ordinary equality operators for its current editor value, including a required field that has not received input yet.validation.null.no-special-syntax: No special nullable-access syntax exists.validation.null.non-nullable-comparison: The comparison is valid even when the schema field is notNullable. Client editors usenullas the initial state of many required fields, and rules are allowed to describe that state before final structural validation.
5.1 is Operator
Owner: validation-rules.md#51-is-operator
-
validation.is.operator: Theisoperator tests the selected Union variant or the selected Enumeration member of a value. -
validation.is.union-runtime: Its result is boolean. On a Union it tests the actual runtime variant. -
validation.is.enumeration: When the operand is an Enumeration,Nameis a member name and the test is true exactly when the stored value selects that member, resolved as the Enumeration itself resolves a stored value (a member'sFixedvalue first, otherwise its index). A null, unset, or unresolvable Enumeration value gives false. A numeric comparison such as[class] == 0is unaffected. -
validation.is.null-false: If the left operand isnull:Cnull is Tevaluates to:
Textfalse
6.2 Cursor traversal
Owner: validation-rules.md#62-cursor-traversal
-
validation.cursor.element-node:all,any,none, andcollectbind a cursor to each concrete element of the arrays selected by their source path. A cursor is the element node itself, not its array index:Call(item in [items], [item/value] >= 0 )
7.1 Boolean Context
Owner: validation-rules.md#71-boolean-context
validation.boolean.null-false:nullis implicitly converted to booleanfalse:
7.2 No General Truthiness
Owner: validation-rules.md#72-no-general-truthiness
-
validation.truthiness.explicit-comparison: Explicit comparison MUST be used:C[count] != null [name] != null [object] != null [array] != null
7.3 First-non-null value fallback
Owner: validation-rules.md#73-first-non-null-value-fallback
validation.fallback.first-non-null: The evaluator returns the first operand whose value is notnull, without changing that value's type. It evaluates no later operand after finding a winner.false, numeric zero, an empty string, an empty array or collection, and an object are all non-null and therefore win. The result isnullonly when every operand evaluates tonull.validation.fallback.static-types: The result's possible static types are the left operand's non-null types plus the right operand's possible types only when the left operand may be null. A chain is left-associative and applies that rule repeatedly.
8. Logical Operators
Owner: validation-rules.md#8-logical-operators
validation.logical.short-circuit:&&and||MUST use short-circuit evaluation.
9. Equality Operators
Owner: validation-rules.md#9-equality-operators
validation.equality.null:nullis an ordinary value for equality comparison.
10. Relational Operators
Owner: validation-rules.md#10-relational-operators
validation.dates.same-semantic-type: Ordinary integers and floating-point Numbers are mutually comparable. DateOnly is comparable only with DateOnly, and DateTime only with DateTime; their storage encodings do not make them comparable with a Number. Strings are comparable only with strings.validation.relational.null-false: A relational comparison involvingnullevaluates tofalse.
11. Arithmetic Operators
Owner: validation-rules.md#11-arithmetic-operators
-
validation.arithmetic.exact: Numeric literals and decoded scalar decimal text are evaluated as exact rational values. Addition, subtraction, multiplication, division, remainder, equality, and ordering preserve the low digits of i64 Money and do not use binary64 intermediates. Division does not round a repeating fraction; remainder uses a quotient truncated toward zero. Each reduced numerator and denominator is limited to 256 decimal digits. Arithmetic exceeding that limit produces null, as does division by zero. This evaluation representation does not change the schema's integer/floating semantic types or any wire encoding. Ordinary numeric UI controls may still use floating-point coordinates; Money bounds and final checks retain exact values throughout. -
validation.arithmetic.result-types: Ordinary integer and floating-point operands use numeric arithmetic. Integer results remain internally typed as integers except that/produces a Number; an operation involving a floating-point Number produces a Number. -
validation.arithmetic.date-overloads: DateOnly and DateTime have these additional, exhaustive arithmetic overloads:TextDateOnly + integer -> DateOnly, shifted by Gregorian calendar days integer + DateOnly -> DateOnly, shifted by Gregorian calendar days DateOnly - integer -> DateOnly, shifted by Gregorian calendar days DateOnly - DateOnly -> Number, elapsed calendar days DateTime + integer -> DateTime, shifted by Unix seconds integer + DateTime -> DateTime, shifted by Unix seconds DateTime - integer -> DateTime, shifted by Unix seconds DateTime - DateTime -> Number, elapsed seconds -
validation.arithmetic.gregorian: DateOnly arithmetic MUST use Gregorian calendar rules: a DateOnly path carrying the wire value20240228plus1evaluates to DateOnly20240229, and plus2evaluates to DateOnly20240301. It MUST NOT add to the numericyyyyMMddencoding. DateOnly/DateTime mixed arithmetic, integer-minus-date, unary+/-on a date, and*,/, or%on a date are invalid. Calendar-month and year shifts and the extraction of a date's year, month, and day are the functions of section 11.1, not operators. -
validation.arithmetic.null: If an arithmetic operand evaluates tonull, the arithmetic result isnull.
11.1 Calendar-date functions
Owner: validation-rules.md#111-calendar-date-functions
-
validation.calendar.fields:year(d)returns the calendar year (1000 through 9999),month(d)the month (1 through 12), andday(d)the day of the month (1 through 31), each as an integer Number. For example,year(d"2024-02-29") == 2024,month(d"2024-02-29") == 2, andday(d"2024-02-29") == 29. -
validation.calendar.add-months:add_months(d, n)returns the DateOnlyncalendar months afterd, or before it whennis negative. The year and month advance bynmonths. The day of the month is kept when the target month has that day; otherwise it becomes the target month's last day:Cadd_months(d"2024-01-31", 1) == d"2024-02-29" add_months(d"2023-01-31", 1) == d"2023-02-28" add_months(d"2024-03-31", -1) == d"2024-02-29" add_months(d"2024-05-31", 1) == d"2024-06-30" add_months(d"2024-01-15", 24) == d"2026-01-15" -
validation.calendar.no-compose(judgment): Because of the day clamp, shifts do not compose:add_months(add_months(d"2024-01-31", 1), 1)isd"2024-03-29", whileadd_months(d"2024-01-31", 2)isd"2024-03-31". A shift by days remains the+and-arithmetic of section 11. -
validation.calendar.null-range: If any argument isnull, the result isnull. A shift whose result falls outside the DateOnly range of years 1000 through 9999 also evaluates tonull, as does day arithmetic beyond that range. -
validation.calendar.pure: The functions are pure (section 17): they read only their arguments, and the language has no current-date value. -
validation.calendar.month-difference(judgment): The language has no month-difference function. A whole-month difference has competing conventions at month ends; compare a shifted date instead. For example, "at leastnmonths fromatob" isadd_months([a], n) <= [b], and the last day of a period ofnmonths beginning on[start]isadd_months([start], n) - 1.
12. Collection Operations
Owner: validation-rules.md#12-collection-operations
validation.collection.concatenation: Arrays selected through multiple parent-array elements are concatenated in deterministic path and element order. A selectednullor missing array contributes no elements.
12.1 all
Owner: validation-rules.md#121-all
validation.collection.all: Returnstrueiff the expression evaluates to booleantruefor every array element.
12.2 any
Owner: validation-rules.md#122-any
validation.collection.any: Returnstrueiff the expression evaluates to booleantruefor at least one array element.
12.3 none
Owner: validation-rules.md#123-none
validation.collection.none: Returnstrueiff the expression evaluates tofalsefor every array element.
12.4 collect
Owner: validation-rules.md#124-collect
validation.collect.predicate-first:collectreturns an ordered collection. Its source remains a current-data path selecting arrays, including arrays reached through parent-array fan-out. For each concrete cursor, it evaluatespredicatefirst. Only Booleantruecauses the projection to be evaluated, once, in that cursor's scope. False or null predicates contribute nothing and do not read the projection.validation.collect.direct-path: A direct square data path retains path-selection behavior: append every selected node in path order, flattening arrays crossed on the path. A missing field selects no node; a selected null field contributes one null. A path ending at a List contributes that List as one value, without flattening its elements.validation.collect.computed-collection: Another expression producing a collection, including nestedcollect, concatenates its items in order at this one boundary. An empty collection contributes no item.validation.collect.computed-value: Another expression producing a scalar, null, object, or List contributes exactly one value, retaining its semantic type and identity. A computed null is one item, even when it came from a missing path. Objects and Lists are not recursively flattened or coerced.validation.collect.order-bag: The result preserves source order, projection order, and duplicates; it is a collection (bag), not a mathematical set. Empty sources and predicates that never evaluate to true produce empty collections.validation.collect.type-guards: Studio narrows projection validation using positive type tests on the cursor or a nested cursor path. The tests may be conjuncts of&&; unrelated conjuncts do not remove their guarantees. For example,[row/document] is Payment && [row/enabled]permits numeric projection paths through the selectedPaymentpayload even when another variant declares an object field with the same name.validation.collect.guard-scope: These facts apply only to thatcollectprojection and its nested scopes; they do not change the schema or escape to later expressions.||, negation and fallback do not establish positive type facts. A guard on another cursor or path cannot narrow this one, and array fan-out remains invalid as anisoperand. Null and absent payloads retain their existing evaluation behavior.validation.collect.client-support(judgment): Computed projections require clients implementing this contract. Older clients support only direct data-path projections and must be updated before an App using computed projections is distributed to them. Schema acceptance alone does not update an installed client.
12.5 is_unique_set
Owner: validation-rules.md#125-isuniqueset
validation.unique.semantics: Returnstrueexactly when no two collection elements compare equal under the ValidationRules==semantics. Empty and single-element collections are unique. Twonullelements are duplicates.validation.unique.fanout-collection: A fan-out path argument such as[rows/key]is always the collection of the nodes it selects, whether that is zero, one or many, including zero when a List on the path isnull; it is never reduced tonullor a single value for this function.validation.unique.not-all(judgment):allis not overloaded for this purpose. Its predicate is evaluated independently for one bound cursor and returns only a Boolean; it has neither collection state nor another-element binding with which to detect duplicates.validation.unique.key-tuple: The cursor binds to each element of the source arrays exactly as forall(section 6.2), and the key expressions are evaluated for that element in order. The element's key is the tuple of their values. The result istrueexactly when no two elements have keys whose corresponding components all compare equal under==. Anullcomponent therefore equals onlynull: two elements that agree on every other component and are bothnullin one component are duplicates, while anulland a non-null component differ.validation.unique.empty: An empty source, a single element, and a missing ornullsource array are unique (section 12.7).validation.unique.bound-keys(judgment): The keys are bound to their element rather than written as separate paths: independently resolved paths form separate collections, and a field absent from one element's active Union payload would shift the later values, pairing components that belong to different elements.
12.6 count
Owner: validation-rules.md#126-count
validation.count.semantics: returns the number of elements in an array or acollectresult.validation.count.fanout: A fan-out path argument such as[rows/key]is always the collection of the nodes it selects, so it counts 0, 1 or more (0 when a List on the path isnull); otherwise, if its argument resolves tonull,count()evaluates tonull.
12.7 Missing or Null Traversal Arrays
Owner: validation-rules.md#127-missing-or-null-traversal-arrays
validation.collection.missing-array: A missing ornullterminal array contributes zero elements to a traversal. Consequently, the usual empty-collection identities apply:allandnonereturntrue,anyreturnsfalse, andcollectreturns an empty collection. Use an explicit path null test when array presence is itself required.
12.8 sum
Owner: validation-rules.md#128-sum
validation.sum.null-leaves:nullleaves are skipped. When nothing is selected, including an empty or missing array or a source whose leaves are allnull, the result is the integer0; a single selected value is its own sum. The sum is therefore a Number for every data instance, and a rule such as[principal] <= sum([contributions/amount])needs no null guard for the collection.validation.sum.exact: Addition is the exact arithmetic of section 11, so the result is independent of order; it isnullonly when it exceeds that section's exact numeric limit. The result is an integer when every summed leaf is integer-typed, and a Number otherwise.
13. String Operations
Owner: validation-rules.md#13-string-operations
-
validation.string.length-bytes: which returns the number of bytes in the string's UTF-8 encoding. Every platform must use this same byte count, matching StringSizeMin/SizeMax. The text is measured as supplied, without Unicode normalization. For example,length("A")is 1,length("é")is 2,length("中")is 3,length("😀")is 4, andlength("e\u0301")is 3. Native-character constraints (MinLengthAbs/MaxLengthAbs) remain a separate Unicode-scalar constraint. -
validation.string.no-character-count:lengthcounts bytes, not characters, by design. A byte bound is the same on every platform and matches what storage, wire messages and native buffers actually hold, so a value accepted on one client can never exceed a limit on another. A character count differs by platform and Unicode version. The language has no character-counting function today. If a rule ever needs one, it will be a separate, explicitly named function;lengthand String Size will keep counting UTF-8 bytes. -
validation.string.length-null: If the input isnull:Textlength(null) -> null
13.1 String resource references
Owner: validation-rules.md#131-string-resource-references
validation.resource.key: takes a FormattedText value and returns its string resource key as a String, or null when the value is null. The key identifies the resource template, not its localized text, so comparing it gives the same result in every locale. Arguments do not change the key.
13.2 Full-string patterns
Owner: validation-rules.md#132-full-string-patterns
validation.matches.full-string:matches(value, pattern)tests the entire String against the portable regex language below. It returns Boolean for two Strings, without trimming, normalization, substring search, flags, captures, replacement or coercion. An empty pattern matches only empty text.validation.matches.null: With two Strings, return Boolean. Empty text matches only a pattern accepting empty text; an empty pattern accepts only empty text. A null argument returns null under existing missing-data/deferral rules. Optional fields can use[] == null || matches([], "[0-9]+"). Complete final rules still require true.validation.matches.static-compile: Studio compiles every statically known pattern, even in an unreachable branch. Malformed syntax, unsupported constructs and compilation limits produce a schema error at the narrowest source location.validation.matches.evaluation-errors: A dynamic pattern compiles when evaluated. Invalid patterns, wrong runtime types and compilation failures are evaluation errors, not false or null. An evaluated error makes the enclosing rule fail, including under!,|or||. Short circuiting still avoids evaluating an unused branch. Thus!matches(value, badPattern)cannot pass. This explicit error distinction must be preserved through the evaluator; the existing missing-value sentinel alone is insufficient. No expression-level exception-catching API is added.
Pattern attribute and lifecycle
Owner: validation-rules.md#pattern-attribute-and-lifecycle
validation.pattern.effective: Use the existing effective-attribute resolver: the field's explicit Pattern wins over its referenced type's Pattern. Existing cumulative ValidationRules remain cumulative.validation.pattern.client-lifecycle: Clients run Pattern through ordinary intrinsic value validation alongside Size, on committed edits and final submission. Temporary incomplete typing may remain editable but cannot be submitted. Import, copy, calculation, restore, Hidden and ReadOnly do not waive validation or erase bad values. Null adds no pattern mismatch; ordinary requiredness still applies.validation.pattern.message: A mismatch uses a standard localized message, such as “Enter a value in the required format.” The field description explains that format. There is no second message-key attribute; use matches in ValidationRules for a custom message. Invalid patterns are configuration/rule errors, not typing errors.validation.pattern.server-validates(judgment): Server Apps still validate untrusted requests independently.
Portable regex language
Owner: validation-rules.md#portable-regex-language
validation.regex.scalars: One regular-language grammar over Unicode scalars, case sensitive and locale independent. No normalization: composed and decomposed accents differ.
Escape and quote contract
Owner: validation-rules.md#escape-and-quote-contract
validation.escape.decode-once: Decode each layer once. The compiler receives regex text, not source code or a JSON-encoded value. Transport JSON escaping is representation only.
Source attribute layer
Owner: validation-rules.md#source-attribute-layer
validation.escape.rust-pattern: For shared semantic rejection, follow the Pattern placement and grammar requirements. Rust: one positional native String literal, cooked or raw, or$CONSTresolving to a native String through existing source-constant machinery. Cooked literals use native Rust escapes; rawr"...",r#"..."#etc do not decode backslashes. Increase delimiter hashes when content includes the current closing quote/hash sequence.validation.source.raw-delimiter-choice: Native Rust source preprocessing and cooked-string line continuations retain their native meaning; they are not regex escapes. For exact LF/CR data, prefer explicit cooked escapes or regex\n/\rin a raw literal. If the content cannot fit a legal raw delimiter, use a cooked literal instead. Bytes and character literals are not accepted as String arguments.validation.escape.typescript-pattern: For shared semantic rejection, follow the Pattern placement and grammar requirements. TypeScript Pattern in JSDoc: one positional double-quoted JSON String literal, or$CONSTresolving a native String constant. Decode JSON once, without trim. Accept exactly JSON escapes\" \\ \/ \b \f \n \r \t \uHHHH; surrogate pairs form one scalar.validation.escape.json-whitespace: JSON whitespace here means only space, tab, LF and CR. Raw U+0000–U+001F inside the JSON literal is invalid; use an escape. Literal Unicode scalars above that range, including U+2028/U+2029, are JSON content, not delimiters. Native source/comment syntax must also permit the spelling. No implicit line continuation or JSDoc line-decoration removal occurs inside a literal; use escaped controls or a constant for multiline patterns.validation.escape.const-argument: For shared semantic rejection, follow the Pattern placement and grammar requirements.$CONSToccupies the whole argument; no interpolation occurs inside quoted patterns, where$remains a regex anchor.validation.escape.jsdoc-delimiters: Scan delimiters while respecting quoted strings and escapes: a regex),@or brace inside a String cannot terminate the annotation. JSDoc*/still ends its comment; represent a literal star/slash sequence using JSON*\/or a native constant. HTML entities have no decoding role. JSON\bis U+0008, not a regex boundary; JSON has no\u{...}escape, so double that backslash.validation.escape.annotation-discovery: This applies to annotation discovery as well as argument extraction:@LibertasPattern("@LibertasHidden()")declares only Pattern. Its contents must never create Hidden or another annotation. A marker is recognized only outside an already recognized annotation's quoted argument. Separate adjacent annotations resume normal scanning after the closing).
ValidationRules literal layer
Owner: validation-rules.md#validationrules-literal-layer
-
validation.literal.rule-escapes: The existing rule lexer permits single and double quotes; the matching unescaped quote closes. A backslash followed by n/r/t emits LF/CR/tab; followed by any other character, it emits that character and drops the backslash. Thus\\emits one backslash,\"quote and\'apostrophe. Missing closing quote or dangling escape is a syntax error. These identity escapes remain part of the rule literal grammar. -
validation.source.enclosing-string-valid: In particular, rule\b \f \v \0decode to ordinary b/f/v/0, and a backslash followed by an actual line break preserves that line break rather than joining lines. Raw controls are accepted by the existing rule lexer; their enclosing source representation must still be valid. To request regex form-feed/vertical-tab escapes, transmit\\f/\\v. For NUL or backspace, transmit regex\\x00/\\x08or an actual scalar; regex\0/\bare unsupported regardless of their meaning in a host regex engine. -
validation.literal.double-backslash: Double every backslash intended for regex: rule"\\d+"transmits regex\d+, whereas"\d+"transmitsd+. Similarly"\\q"transmits an invalid regex escape, while"\q"transmits ordinary q. Rule\u/\xare identity escapes: write"\\u{1F600}"to transmit that regex Unicode escape. -
validation.literal.outer-envelope: An outer Rust ValidationRules literal decodes first; prefer raw Rust. Existing TypeScript LibertasValidationRules JSDoc preserves raw quoted annotation content, unlike the new JSON-decoded Pattern. Use a single-quoted outer envelope and double-quoted rule strings:Rust#[libertas_validation_rules(r#"matches([], "\\d+") : "DIGITS";"#)] pub digits: String,TypeScript/** @LibertasValidationRules('matches([], "\\d+") : "DIGITS";') */ digits: string; -
validation.literal.mechanical-encoder: For a mechanical rule-literal encoder, choose double quotes and escape every backslash as\\, both quote kinds as\"and\', and LF/CR/tab as\n/\r/\t. Other scalars remain unchanged. Escaping the apostrophe is redundant for the rule's double-quote delimiter but makes the same text safe inside the legacy TypeScript single-quoted envelope. Preserve that rule text verbatim in the envelope; do not apply JSON or JavaScript string encoding to it. JSDoc's physical*/restriction still applies to the whole comment, including the legacy envelope. A regex slash can instead be spelled\x2F(double its backslash in the rule). Native constants use their existing annotation-specific resolution; they do not acquire interpolation semantics.
Regex layer and exact-value examples
Owner: validation-rules.md#regex-layer-and-exact-value-examples
validation.regex.no-redecode: The regex compiler never re-decodes JSON, source strings, rule literals, constants or HTML. A String obtained from a path already contains decoded text.validation.regex.quotes: Quotes/apostrophes are not regex metacharacters. Escape them only for their source/rule delimiter or choose a raw delimiter. A backslash before a closing quote is interpreted by the enclosing literal first; literal final backslash requires the complete doubling above. Regex; : @ ) { }inside a quoted argument cannot end a call, rule or annotation.
14.1 DateOnly literals
Owner: validation-rules.md#141-dateonly-literals
validation.date-literal.constant: A literal is a constant; the language has no current-date value (section 17).validation.date-literal.no-datetime: There is no DateTime literal. A written date and time names a different instant in each time zone, and rules have no time-zone input; instants come from DateTime paths and DateTime arithmetic.
14.2 Bitflags membership
Owner: validation-rules.md#142-bitflags-membership
-
validation.bitflags.semantics: For a non-null host valuehand the member'sFixedvaluem, the result is the Boolean(h & m) != 0: true when that bit is set, whatever other bits are set.mis a schema constant, so evaluation stays pure (section 17). Anullhost yieldsnull, which isfalsein Boolean context (section 7.1); the integer itself is never converted to a Boolean. Bitflags hosts are non-negative: a negative host value is outside the catalog and the test yieldsnull, as for a null host. Where absence matters, combine the test with an explicit null test or a fallback:C[facts] != null && !([facts] has PersonFacts.Student) ([facts] has PersonFacts.ReleasedToYou) | false
15. Operator Precedence
Owner: validation-rules.md#15-operator-precedence
-
validation.precedence.table: From highest precedence to lowest:Text1. () 2. path access 3. unary ! + - 4. * / % 5. + - 6. | 7. is has 8. < <= > >= 9. == != 10. && 11. || -
validation.precedence.has-fallback:hasshares theislevel: both test one value against a schema name and produce a Boolean, so arithmetic and|bind first, while comparisons, equality, and logical operators take the result. A fallback for ahasresult therefore needs parentheses, as in([facts] has PersonFacts.Student) | false. -
validation.precedence.function-primary: A function call or collection operation is a primary expression: its parenthesized arguments bind first, soyear([start]) + 1adds one to the year andadd_months([start], 24) - 1is the day before the shifted date. -
validation.precedence.parentheses(judgment): Parentheses SHOULD be used when they improve clarity.
16. Static Rule Validation
Owner: validation-rules.md#16-static-rule-validation
validation.static.path-roots: Square paths are validated against the attribute evaluation root. Reference paths are syntax-checked but their value type remains unknown and nullable, because the referenced response may use a different compatible schema.validation.static.nullable-warning: Studio warns when an applicable rule reads a nullable host or path prefix without explicit absence handling. This includes a named-type rule at a nullable use and a selected nullable Union payload. Under sections 1, 3 and 4.4, cumulative rules still evaluate with a present null host; descendant reads yield null and a relational comparison then returns false (section 10).validation.static.absence-not-pass: Absence never makes a rule pass automatically. For an optional record, write[] == null || ...when the rule should accept its absence.validation.static.union-paths: A path does not need to apply to every union alternative.
17. Evaluation Purity
Owner: validation-rules.md#17-evaluation-purity
validation.purity.must: Validation expressions MUST be pure.validation.purity.forbidden-side-effects: A rule MUST NOT:validation.purity.reference-data: Reading the suppliedreferencedDatais permitted and does not access unprovided ambient state. Evaluation does not fetch a response or consult interaction history.validation.purity.deterministic-results: MUST always produce the same result.
18. Validation Result
Owner: validation-rules.md#18-validation-result
-
validation.result.same-language: The same expression language and evaluation semantics apply when a client performs incremental validation during editing and when it performs final validation. There is no separate relaxed or final expression dialect. Incremental validation reports the rule result for the current, possibly incomplete datum after omitting declarations whose leading directive isFinalOnly=true. Final validation evaluates every declaration, including those final-only rule sets, and combines the result with all applicable static schema checks. -
validation.result.top-level-null: Becausenullbecomesfalsein boolean context:Texttop-level null -> INVALID -
validation.result.message: ForINVALID, a Libertas client MUST display the failing rule pair's localizedStringResourcesmessage. -
validation.result.first-failure: Evaluate applicable cumulative rule sets in their supplied order and pairs within each set in declaration order; stop at the first false rule. Skip final-only sets during incremental evaluation. Reporting the failure MUST preserve normal short-circuit behavior and MUST NOT evaluate skipped branches, later rules, or later quantifier elements to construct an explanation. -
validation.result.structural-diagnostic: A separate structural diagnostic may still reject a required field that remainsnullat submission time. -
validation.result.final-valid: LetStaticValidinclude mandatory/nullability, type, range, size, uniqueness, and every other ordinary schema constraint. These checks receive the samereferencedDatawhere applicable, including dynamic bounds. Final task data is accepted iff: -
validation.output-only.excluded: An OutputOnly request node is outside client validation: the client checks neither its static constraints nor the rule sets hosted on it or inside it, incrementally or finally. Rule sets hosted elsewhere that read it evaluate as usual.
19. Constraint Completeness
Owner: validation-rules.md#19-constraint-completeness
validation.reference.changes-result: ChangingreferencedDatamay change this subset without changing the schema, rule, or current data. For example, the rule{$.maximum} == null || [] <= {$.maximum} : "ABOVE_MAXIMUM";accepts50when the supplied maximum is100, rejects it when the maximum is40, and accepts it when there is no reference context because of its explicit null guard.
20.7 Named type, field, and function ownership
Owner: validation-rules.md#207-named-type-field-and-function-ownership
validation.example.cumulative-ownership: Thepercentagevalue must satisfy both the named-type and field-local rules. The function rule sees minimum and maximum in one root. The explicit null guards are valid even though the source types are not nullable, because the editor may not have received user input yet. If one of those arguments remainsnullat final submission, its validation rule may pass while the independent mandatory-field check still rejects the task data.
20.10 Bitflags membership
Owner: validation-rules.md#2010-bitflags-membership
validation.bitflags.no-bit-arithmetic(judgment): When the person's facts includeReleasedToYou, the support amount is required, regardless of which other flags are set. Without the operator the same rule needs the bit position, as in[facts] % 2 == 1.
21. Core Design Principles
Owner: validation-rules.md#21-core-design-principles
validation.principles.cursor-traversal: Arrays use cursor traversal and collection operators rather than general-purpose loops.
Rules Studio enforces
Studio rejects a source that breaks one of these rules. The owner holds the full rule.
3. Rule Set
Owner: validation-rules.md#3-rule-set
validation.syntax.pair: Every Boolean expression is followed by one colon, a quoted StringResources key and a semicolon; a bare expression is invalidvalidation.syntax.message-key: Each message key is declared in the package StringResources with a non-empty default-locale valuevalidation.finalonly.directive: FinalOnly, when present, is the single leading directive, written once, with the lowercase value true or false
3.1 Dynamic numeric ranges
Owner: validation-rules.md#31-dynamic-numeric-ranges
validation.range.one-expression: A RangeMin or RangeMax holds exactly one value expression, with no message pair or FinalOnly directivevalidation.range.bound-type: A range expression produces a bound of the host's semantic type, never a Boolean predicate
5.1 is Operator
Owner: validation-rules.md#51-is-operator
validation.is.static-names: Studio rejects anisname that no member of a known Enumeration or no variant of a known Union can match
7.2 No General Truthiness
Owner: validation-rules.md#72-no-general-truthiness
validation.boolean.no-truthiness: Non-null values other than Booleans never convert to Boolean; use an explicit comparison
9. Equality Operators
Owner: validation-rules.md#9-equality-operators
validation.equality.semantic-types: Equality uses semantic types: Numbers compare with Numbers, DateOnly only with DateOnly, DateTime only with DateTime
10. Relational Operators
Owner: validation-rules.md#10-relational-operators
validation.relational.compatible: Relational operands are compatible: Numbers with Numbers, a DateOnly or DateTime with the same type, Strings with Strings
11.1 Calendar-date functions
Owner: validation-rules.md#111-calendar-date-functions
validation.calendar.dateonly-argument:year,month,day,add_monthsandadd_yearstake a DateOnly; a Number, String, collection or DateTime is a static errorvalidation.calendar.integer-count: Theadd_monthsoradd_yearscount is integer-typed; a floating-point Number is a static error
12. Collection Operations
Owner: validation-rules.md#12-collection-operations
validation.collection.array-sources: Traversal sources are data paths that resolve only to arrays
12.4 collect
Owner: validation-rules.md#124-collect
validation.collect.guard-specificity: A deeper path uses the most specific compatible guard; repeated guards intersect and contradictory guards are rejectedvalidation.cursor.lexical: Cursors are lexical: inner traversals may read outer cursors but cannot reuse a name in scope or leak it
12.5 is_unique_set
Owner: validation-rules.md#125-isuniqueset
validation.unique.key-required: The cursor form ofis_unique_setneeds at least one keyvalidation.unique.key-single-value: Eachis_unique_setkey produces one value per element, not a collection
12.8 sum
Owner: validation-rules.md#128-sum
validation.sum.numeric-leaves: Every leafsumselects or computes is an ordinary integer or floating-point Number, including Money
13.1 String resource references
Owner: validation-rules.md#131-string-resource-references
validation.resource.formattedtext-argument:resource()requires a FormattedText argumentvalidation.resource.declared-key: A string literal compared with aresource()result is a key the package StringResources declares
13.2 Full-string patterns
Owner: validation-rules.md#132-full-string-patterns
validation.matches.arguments:matches()takes exactly two String or null arguments; Number, date, Boolean, collection and FormattedText arguments are rejectedvalidation.matches.text-free: Text-free presentation conditions rejectmatches()
Pattern attribute and lifecycle
Owner: validation-rules.md#pattern-attribute-and-lifecycle
validation.pattern.placement: Pattern goes once on an effective String node, never on packages, functions, containers, Enumerations, numbers, dates, Bytes or FormattedTextvalidation.pattern.studio-checks: Studio validates Pattern placement, multiplicity and grammar, and that non-null Default and Fixed values match
Portable regex language
Owner: validation-rules.md#portable-regex-language
validation.regex.classes: In a class only a leading^negates and-is literal first or last;[and]are escaped; empty, nested and reversed classes are rejectedvalidation.regex.punctuation: Outside classes the listed punctuation is syntax; unmatched closers, misplaced quantifiers and malformed counts are invalidvalidation.regex.escapes: Unknown escapes, surrogate-pair escapes, flags, backreferences, lookaround and similar host-engine features are errorsvalidation.regex.hex-escapes: Hex escapes take exactly 2 or 4 digits, or 1–6 braced digits; incomplete, out-of-range and surrogate escapes are errorsvalidation.regex.limits: Patterns are limited to 4096 scalars, 64 group nesting, repeat counts up to 10000 and 65536 compiled states
Source attribute layer
Owner: validation-rules.md#source-attribute-layer
validation.escape.unpaired-surrogate: An unpaired surrogate in decoded regex text is a Studio semantic error
14. Expression Grammar
Owner: validation-rules.md#14-expression-grammar
validation.grammar.ebnf: Rule sets follow the normative grammar;length,resource,year,monthanddaytake one argument,add_months,add_yearsandmatchestwo
14.1 DateOnly literals
Owner: validation-rules.md#141-dateonly-literals
validation.date-literal.valid-date: A DateOnly literal names a valid Gregorian date with a year from 1000 through 9999
14.2 Bitflags membership
Owner: validation-rules.md#142-bitflags-membership
validation.bitflags.host-operand: The left operand ofhasis a current-data path to a Bitflags host, without array fan-outvalidation.bitflags.member-operand: The right operand ofhasis one wordCatalog.Member: the host's own catalog type name and one of its nonzero members as declaredvalidation.bitflags.one-member:hastakes one nonzero member; test several flags with&&or||and the empty selection with== 0
16. Static Rule Validation
Owner: validation-rules.md#16-static-rule-validation
validation.static.compatible-operators: Studio verifies that operators other than the first-non-null operator apply to compatible typesvalidation.static.boolean-results: Every complete rule and everyall,any,noneandcollectpredicate produces a Boolean or possibly nullvalidation.static.is-type: Anistest names a schema-defined typevalidation.static.cursor-use: A cursor variable is only the first segment of a data path, within its lexical traversalvalidation.static.resolvable-path: Every path is resolvable through at least one schema-valid structural route