Version: 1.2.0 Date: January 2026 Status: Draft
License: This annex is part of the Open Okyline Language Specification and is subject to the same license terms (CC BY-SA 4.0). See the Core Specification for full license details.
This annex defines the Okyline Expression Language,
a pure and deterministic language for expressing computed validations.
It enables business invariants such as cross-field calculations (e.g.,
total == subtotal * (1 + taxRate)) and conditional logic
based on field values. Expressions are declared in a
$compute block and referenced in field constraints using
the (%ExpressionName) syntax.
This annex defines the Okyline Expression Language,
used inside $compute blocks and computed field
constraints.
It specifies:
$compute block)(%Name))Implementations claiming Okyline 1.2.0 Level 3 conformance (computed business invariants) MUST implement the rules described in this annex.
The Expression Language is an integral component of Okyline, not a separate language. It extends the core validation model with computation and conditional logic while remaining pure, deterministic, and side-effect-free.
Although this annex is part of the Okyline 1.2.0 Draft specification, the expression language defined here is considered stable for practical use.
The Expression Language can be used independently of validation, for example in documentation tools or template generators, but only the behaviors defined in this annex are considered part of the Okyline 1.2.0 normative model.
The Okyline Expression Language defines the grammar and semantics
used in $compute blocks and computed constraints.
This annex is normative, meaning its contents define the expected behavior of conforming implementations of Okyline 1.2.0.
Expressions are pure, deterministic, and evaluated within the context of the object where they appear. All functions are null-safe and side-effect free.
$computeOkyline reserves the special block $compute as a
container for named expressions. It is declared at the root
level, alongside $oky.
{
"$compute": {
"ExpressionName": "expression body"
},
"$oky": {
...
}
}Example:
{
"$oky": {
"invoice": {
"subtotal": 100.0,
"taxRate": 0.2,
"total|(%ValidTotal)": 120.0
}
},
"$compute": {
"TaxAmount": "subtotal * taxRate",
"ValidTotal": "total == subtotal + %TaxAmount"
}
}$compute is declared at the root
level, not inside $oky.$compute is optional; when present, it
MUST be a JSON object.(%Name)To validate a field using a computed expression, reference it with
the (%ExpressionName) syntax in the field’s constraint
declaration.
"fieldName|(%ExpressionName)": exampleValue
Example:
{
"$oky": {
"order": {
"quantity|(>0)": 5,
"unitPrice|(>0)": 20.0,
"total|(%CheckTotal)": 100.0
}
},
"$compute": {
"CheckTotal": "total == quantity * unitPrice"
}
}When a field references a computed expression:
| Result | Validation |
|---|---|
true |
Passes |
false |
Fails — COMPUTE_VALIDATION_FAILED |
null |
Fails — COMPUTE_TYPE_ERROR |
| Non-boolean | Fails — COMPUTE_TYPE_ERROR |
Error reporting SHOULD include:
order.total)CheckTotal)(%Name) can be combined with other constraint types:
"email|@ (%ValidEmail) ~^[^@]+@[^@]+$~|Contact email": "user@example.com"The field email must satisfy all constraints:
@)ValidEmail expression
((%ValidEmail))~...~)Constraint type exclusivity:
Since each constraint type can appear only once per field,
(%Name) cannot be combined with other
parenthetical constraints like (>0) or
(<100). Both use the (...) syntax and
occupy the same constraint slot.
// INVALID - two parenthetical constraints
"total|@ (>0) (%ValidTotal)": 120.0
// VALID - numeric check must be inside the expression
"total|@ (%ValidTotal)": 120.0If you need both a range check and a computed validation, include the range check within the computed expression itself:
"$compute": {
"ValidTotal": "total > 0 && total == subtotal * (1 + taxRate)"
}Constraint evaluation order:
@, {...},
~...~)(%Name))Expressions are evaluated in the context of a JSON object. All fields of the current object are accessible by name.
For a field constraint (%ExpressionName), the expression
is evaluated in the context of the parent object
containing that field.
{
"$oky": {
"invoice": {
"subtotal": 100.0,
"taxRate": 0.2,
"total|(%ValidTotal)": 120.0
}
},
"$compute": {
"ValidTotal": "total == subtotal * (1 + taxRate)"
}
}In this example, ValidTotal is evaluated with access
to:
subtotal → 100.0taxRate → 0.2total → 120.0Field resolution rules:
subtotal,
taxRateaddress.citynullnull
(null-safe navigation)When using aggregation functions (sum,
average, min, max, etc.), the
second parameter expression is evaluated for each
element in the collection.
{
"$oky": {
"order": {
"items|[1,100]": [
{"qty": 2, "price": 10.0},
{"qty": 3, "price": 15.0}
],
"total|(%CheckTotal)": 65.0
}
},
"$compute": {
"LineTotal": "qty * price",
"CheckTotal": "total == sum(items, %LineTotal)"
}
}In this example:
LineTotal is evaluated in the context of each
item (has access to qty and
price)CheckTotal is evaluated in the context of
order (has access to items and
total)sum(items, %LineTotal) iterates over
items, evaluates %LineTotal for each, and
returns the sumExpressions support path expressions to access fields beyond the immediate validation context.
Path expression syntax and resolution rules are defined in §6.3.13 Field Path Expressions of the core specification.
| Prefix | Resolves From |
|---|---|
| (none) | Current context |
this. |
Current context (explicit) |
parent. |
Parent object |
root. |
Document root |
Example:
{
"$compute": {
"ValidDiscount": "discount <= parent.maxDiscount"
}
}it VariableWhen an expression is evaluated as a field
constraint ((%Name)), the variable it
references the value of the field being validated.
Example:
{
"$compute": {
"IsPositive": "it > 0"
},
"$oky": {
"quantity|(%IsPositive)": 10,
"price|(%IsPositive)": 99.99
}
}Scope: it is defined in the constraint
expression and propagates to any referenced expression
(%Name). In contexts where it is not defined
(aggregations, standalone evaluations), referencing it
resolves to null.
expression ::= logical_or
logical_or ::= logical_and ( "||" logical_and )*
logical_and ::= equality ( "&&" equality )*
equality ::= comparison ( ("==" | "!=" | "===" | "!==") comparison )*
comparison ::= null_coalescing ( (">" | "<" | ">=" | "<=") null_coalescing )*
null_coalescing ::= addition ( "??" addition )*
addition ::= multiplication ( ("+" | "-") multiplication )*
multiplication ::= unary ( ("*" | "/") unary )*
unary ::= ("!" | "-") unary | primary
primary ::= literal | identifier | compute_ref | function_call | "(" expression ")"
compute_ref ::= "%" identifier
function_call ::= identifier "(" [ arguments ] ")"
arguments ::= expression ( "," expression )*
literal ::= number | string | boolean | null
identifier ::= letter ( letter | digit | "_" )*
| Operator | Type | Description | Example | Null Behavior |
|---|---|---|---|---|
?? |
Null coalescing | Returns left if non-null, otherwise right | price ?? 0 → 0 |
Short-circuits; high precedence |
+ |
Arithmetic | Addition | 2 + 3 → 5 |
Null propagates — Exception: string concatenation treats null as
"" |
- |
Arithmetic | Subtraction | 5 - 2 → 3 |
Null propagates |
* |
Arithmetic | Multiplication | 3 * 2 → 6 |
Null propagates |
/ |
Arithmetic | Division | 6 / 2 → 3.0 |
Null propagates; division by zero → null |
> < >=
<= |
Comparison | Numeric/lexicographic | age > 18 |
Returns null if either operand is null |
== != |
Equality | Value equality | "A" == "A" |
null == null → true; null == x →
false |
=== !== |
Strict equality | IEEE-754 bit-exact | 1.0 === 1.0 |
Strict comparison (NaN !== NaN) |
&& |
Logical | Conjunction | a && b |
null → false |
|| |
Logical | Disjunction | a || b |
null → false |
! |
Logical | Negation | !true → false |
!null → true |
? : |
Ternary | Conditional | x > 10 ? "hi" : "lo" |
Condition null → false |
In case of any discrepancy, the operator precedence defined in the grammar (§C.5) MUST take precedence.
| Operator | Type | Description | Example |
|---|---|---|---|
?? |
Null coalescing | Returns the left operand if it is non-null; otherwise returns the right operand (short-circuit evaluation). | price ?? 0 → 0 (if price is null) |
Precedence: The ?? operator has high precedence, binding tighter than both comparison operators and arithmetic operators.
Short-circuit evaluation: The right operand is not evaluated if the left operand is non-null.
Examples:
// If price is null, use default of 10
finalPrice = price ?? 10
// Chain multiple coalescings
value = a ?? b ?? c ?? 0 // First non-null value or 0
// In arithmetic context
total = (price ?? 0) * (quantity ?? 1)| Function | Description | Example |
|---|---|---|
date(dateString, pattern?) |
Parses a string into a LocalDate (default pattern
yyyy-MM-dd). |
date("2024-03-15") → LocalDate(2024-03-15) |
formatDate(date, pattern?) |
Formats a LocalDate to string (yyyy-MM-dd
default). |
formatDate(date("2024-03-15"), "dd/MM/yy") →
"15/03/24" |
today() |
Returns the current system date. | today() → 2025-11-05 |
daysBetween(start, end) |
Number of days difference (end - start). |
daysBetween("2024-03-15","2024-03-18") → 3 |
plusDays(date, days) |
Adds days to a date. | plusDays("2024-02-28",1) →
"2024-02-29" |
minusDays(date, days) |
Subtracts days. | minusDays("2024-03-01",1) →
"2024-02-29" |
plusMonths(date, months) |
Adds months. | plusMonths("2024-01-31",1) →
"2024-02-29" |
minusMonths(date, months) |
Subtracts months. | minusMonths("2024-03-31",1) →
"2024-02-29" |
plusYears(date, years) |
Adds years. | plusYears("2023-03-15",1) →
"2024-03-15" |
minusYears(date, years) |
Subtracts years. | minusYears("2024-03-15",1) →
"2023-03-15" |
isWeekend(date) |
True if Saturday or Sunday. | isWeekend("2024-03-16") → true |
isLeapYear(date) |
True if leap year. | isLeapYear("2024-03-15") → true |
year(date) |
Extracts year. | year("2024-03-15") → 2024 |
month(date) |
Extracts month. | month("2024-03-15") → 3 |
day(date) |
Extracts day of month. | day("2024-03-15") → 15 |
| Function | Description | Example |
|---|---|---|
isNullOrEmpty(s) |
True if s is null or empty. |
isNullOrEmpty("") → true |
isEmpty(s) |
True if string length is 0. | isEmpty("") → true |
substring(s,start,len) |
Returns substring from start. | substring("Hello",1,3) → "ell" |
substringBefore(s,delim) |
Part before first occurrence. | substringBefore("a:b:c",":") → "a" |
substringAfter(s,delim) |
Part after first occurrence. | substringAfter("a:b:c",":") → "b:c" |
replace(s,target,repl) |
Replace all occurrences. | replace("foo bar foo","foo","baz") →
"baz bar baz" |
trim(s) |
Removes leading/trailing spaces. | trim(" hi ") → "hi" |
length(s) |
String length (null → 0). | length("hey") → 3 |
startsWith(s,prefix) |
Checks prefix. | startsWith("hello","he") → true |
endsWith(s,suffix) |
Checks suffix. | endsWith("hello","lo") → true |
contains(s,search) |
True if substring found. | contains("banana","an") → true |
toUpperCase(s) |
Converts to upper case. | toUpperCase("Hi") → "HI" |
toLowerCase(s) |
Converts to lower case. | toLowerCase("Hi") → "hi" |
capitalize(s) |
Uppercases first character. | capitalize("hello") → "Hello" |
decapitalize(s) |
Lowercases first character. | decapitalize("Hello") → "hello" |
padStart(s,len,ch) |
Left pads string. | padStart("7",3,"0") → "007" |
padEnd(s,len,ch) |
Right pads string. | padEnd("7",3,"0") → "700" |
repeat(times,ch) |
Repeats char. | repeat(5,"*") → "*****" |
indexOf(s,sub) |
First index of substring. | indexOf("abracadabra","bra") → 1 |
indexOfLast(s,sub) |
Last index of substring. | indexOfLast("abracadabra","bra") → 8 |
String functions use defensive index handling to prevent runtime errors and ensure deterministic behavior.
"", index -1, etc.).substring("Hello", -10, 3) → "Hel" // negative start clamped to 0
substring("Hello", 1, -5) → "" // negative length treated as 0
substring("Hello", 100, 5) → "" // start beyond length → empty
substring("Hello", 1, 999) → "ello" // end clamped to string length
substring("Hello", 2, 0) → "" // zero length → empty
indexOf("abc", "z") → -1 // not found
padStart("Hi", 1, "x") → "Hi" // already long enough (no truncation)substring(s, start, length) is
interpreted as a length, not an end index.| Function | Description | Example |
|---|---|---|
abs(x) |
Absolute value. | abs(-5) → 5 |
sqrt(x) |
Square root. | sqrt(9) → 3.0 |
floor(x,scale?) |
Rounds down (optional decimals). | floor(3.1415,2) → 3.14 |
ceil(x,scale?) |
Rounds up. | ceil(3.1415,2) → 3.15 |
round(x,scale?,mode?) |
Round with mode (HALF_UP default). |
round(3.5,0,"HALF_EVEN") → 4.0 |
mod(a,b) |
Remainder of division. | mod(10,3) → 1 |
pow(base,exp) |
Power function. | pow(2,3) → 8.0 |
log(x) |
Natural logarithm. | log(2.71828) → 1.0 |
log10(x) |
Base-10 logarithm. | log10(1000) → 3.0 |
toInt(v) |
Converts to integer. | toInt(3.7) → 4 |
toNum(v) |
Converts to number. | toNum("42") → 42.0 |
toStr(v) |
Converts to string. | toStr(42) → "42" |
| Function | Description | Example |
|---|---|---|
sum(collection, expr) |
Sums values of expression evaluated for each element in collection. | sum(lines, price) → 120.0 |
average(collection, expr) |
Average of expression evaluated for each element in collection. | average(lines, quantity) → 5.4 |
min(collection, expr) |
Minimum value of expression in collection. | min(scores, score) → 12 |
max(collection, expr) |
Maximum value of expression in collection. | max(scores, score) → 99 |
count(collection) |
Count of non-null elements in collection. | count(items) → 5 |
countAll(collection) |
Count of all elements in collection (including null). | countAll(items) → 7 |
countIf(collection, expr) |
Count of elements where expression evaluates to true. | countIf(users, active) → 3 |
Note: The %identifier syntax is not a
function but a reference operator for accessing compute
expressions. See C.10.1 Compute
Reference Syntax for details.
%identifierThe %identifier syntax allows expressions to reference
other computed expressions defined in the same $compute
block.
Syntax:
%<identifier>
Where <identifier> is the name of a compute
expression defined in the same $compute block.
Semantics:
%ComputeName evaluates the expression named
ComputeName from the current $compute
block% prefix distinguishes compute references from
field accessesExamples:
{
"$compute": {
"BasePrice": "1000",
"Tax": "%BasePrice * 0.2",
"Total": "%BasePrice + %Tax"
}
}In this example:
Tax references BasePrice using
%BasePriceTotal references both BasePrice and
Tax using %BasePrice and
%TaxAggregation Functions:
When using aggregation functions (such as sum,
map, filter), the second parameter can be:
%identifier
to reference a compute expression){
"$compute": {
"LineTotal": "netAmount * (1 + vat)",
"SubTotal": "sum(items, netAmount)",
"TotalWithFees": "sum(items, netAmount * 1.2)",
"InvoiceTotal": "sum(lines, %LineTotal)"
}
}Benefits:
fieldName) and compute references
(%ComputeName)field|(%ComputeName))Constraints:
% must be followed immediately by a valid
identifier$compute blockImplementation Note:
The parser transforms %identifier into an internal
representation at parse time. The exact internal mechanism is
implementation-defined, but the observable behavior must match the
semantics described above.
null.toNum,
toStr, etc.).null.null evaluate to false
unless explicitly tested.COMPUTE_ERROR, INVALID_ARGUMENT, etc.).The Okyline Expression Language guarantees exact decimal arithmetic suitable for financial calculations.
Guaranteed behaviors:
0.1 + 0.2 = 0.3
(exactly)(0.1 + 0.2) * 3 = 0.9 (exactly)Examples:
0.1 + 0.2 → 0.3 // exact, not 0.30000000000000004
19.99 * 3 → 59.97 // exact
100.00 / 3 → 33.333333 // rounded to 6 decimals
1000 * 0.196 → 196 // exactRationale:
Financial and business applications require deterministic decimal arithmetic. Implementations MUST NOT rely on binary floating-point (IEEE 754) for intermediate calculations, as this produces rounding artifacts unacceptable in financial contexts.
Manual Rounding Control:
When finer control over rounding is required, use the
round(x, scale?, mode?) function to explicitly specify the
number of decimal places and rounding mode.
// Default: 6 decimals, HALF_UP
total * vatRate → 196.000000
// Explicit rounding to 2 decimals for display
round(total * vatRate, 2) → 196.00
// Banker's rounding (HALF_EVEN) for financial compliance
round(amount, 2, "HALF_EVEN") → 2.50
// Round down for conservative estimates
round(estimate, 0, "DOWN") → 99Available rounding modes: HALF_UP (default),
HALF_DOWN, HALF_EVEN, UP,
DOWN, CEILING, FLOOR.
Arithmetic operations follow standard type promotion rules, preserving the most precise type from the operands.
Rules:
| Operation | Result Type |
|---|---|
int + int |
int |
int + decimal |
decimal |
decimal + decimal |
decimal |
int * int |
int |
int * decimal |
decimal |
int / int |
decimal (division always returns decimal) |
Examples:
5 + 7 → 12 // int + int → int
5 + 7.0 → 12.0 // int + decimal → decimal
5.0 + 7.0 → 12.0 // decimal + decimal → decimal
6 / 2 → 3.0 // division always returns decimal
6 * 2 → 12 // int * int → int
6 * 2.0 → 12.0 // int * decimal → decimalRationale:
This behavior mirrors standard programming language conventions (Java, Python, SQL) and ensures predictable results. Integer operations remain integers when possible, preserving the semantic meaning of whole numbers.
Arithmetic operations (+, -,
*, /) follow three-valued
logic (similar to SQL semantics):
+
treats null as the empty string "".Examples:
10 + null → null
null * 5 → null
null / 2 → null
100 - null → null
// String concatenation exception
"Hello" + null → "Hello"
null + " World" → " World"Comparison operators (<, <=,
>, >=) return null if
either operand is null.
10 > null → null
null <= 5 → null
null > null → nullEquality operators have specific null handling:
null == null → truenull != null → falsenull == <any-value> → falsenull != <any-value> → trueNote: For numeric equality (==), values
are compared with implicit rounding to 6 decimal places
using HALF_UP rounding mode.
Boolean operations require boolean operands:
null is treated as false in boolean
contexts.false.null && true → false
null || true → true
!null → trueFunctions handle null arguments according to their specific semantics:
"".user.address.city → null // if user or address is nullThe null propagation semantics for arithmetic operations follow
SQL/database conventions, where operations involving
unknown values (null) produce unknown results (null). This prevents
silent calculation errors and makes null handling
explicit via the ?? operator.
| Feature | Syntax | Description |
|---|---|---|
| Declaration | "$compute": {...} |
Root-level block for named expressions |
| Field constraint | field\|(%Name) |
Validate field using expression |
| Compute reference | %Name |
Reference another expression in $compute |
| Field access | fieldName |
Access field in current context |
| Nested access | obj.field |
Null-safe nested field access |
| Parent access | parent.field |
Access field in parent object (§6.3.13) |
| Root access | root.field |
Access field at document root (§6.3.13) |
| Field value | it |
Value of field being validated (§C.4.4) |
| Null coalescing | a ?? b |
Return b if a is null |
| Expression Result | Validation |
|---|---|
true |
Pass |
false |
Fail — COMPUTE_VALIDATION_FAILED |
null |
Fail — COMPUTE_TYPE_ERROR |
| Non-boolean | Fail — COMPUTE_TYPE_ERROR |
| Category | Operators |
|---|---|
| Arithmetic | +, -, *, / |
| Comparison | >, <, >=,
<= |
| Equality | ==, !=, ===,
!== |
| Logical | &&, \|\|, ! |
| Null coalescing | ?? |
| Ternary | ? : |
| Category | Functions |
|---|---|
| Date | date, today, daysBetween,
plusDays, minusDays, plusMonths,
minusMonths, plusYears,
minusYears, formatDate,
isWeekend, isLeapYear, year,
month, day |
| String | length, substring,
substringBefore, substringAfter,
replace, trim, startsWith,
endsWith, contains, toUpperCase,
toLowerCase, capitalize,
decapitalize, padStart, padEnd,
repeat, indexOf, indexOfLast,
isEmpty, isNullOrEmpty |
| Numeric | abs, sqrt, floor,
ceil, round, mod,
pow, log, log10,
toInt, toNum, toStr |
| Aggregation | sum, average, min,
max, count, countAll,
countIf |
| Context | Behavior |
|---|---|
Arithmetic (+, -, *,
/) |
Null propagates |
String concatenation (+) |
Null treated as "" |
Comparisons (<, >, etc.) |
Returns null |
Equality (==) |
null == null → true |
Logical (&&, \|\|) |
Null treated as false |
| Division by zero | Returns null |
{
"$okylineVersion": "1.2.0",
"$version": "1.0.0",
"$title": "Invoice Schema with Computed Validation",
"$oky": {
"invoice": {
"invoiceId|@ ~$InvoiceId~": "INV-2025-0001",
"issueDate|@ ~$Date~": "2025-06-15",
"dueDate|@ ~$Date~ (%DueDateAfterIssue)": "2025-07-15",
"customer": {
"name|@ {2,100}": "Acme Corporation",
"email|@ ~$Email~": "billing@acme.com"
},
"lines|@ [1,100]": [
{
"description|@ {2,200}": "Consulting services",
"quantity|@ (1..1000)": 10,
"unitPrice|@ (>0)": 150.00,
"lineTotal|@ (%CheckLineTotal)": 1500.00
},
{
"description|@ {2,200}": "Travel expenses",
"quantity|@ (1..1000)": 1,
"unitPrice|@ (>0)": 350.00,
"lineTotal|@ (%CheckLineTotal)": 350.00
}
],
"subtotal|@ (%CheckSubtotal)": 1850.00,
"taxRate|@ (0..0.5)": 0.20,
"taxAmount|@ (%CheckTaxAmount)": 370.00,
"total|@ (%CheckTotal)": 2220.00,
"amountPaid|(>=0)": 0.00,
"balance|(%CheckBalance)": 2220.00,
"status|@ ('DRAFT','SENT','PAID','OVERDUE')": "DRAFT",
"$requiredIf status('PAID')": ["paymentDate"],
"paymentDate|~$Date~": "2025-07-10"
}
},
"$compute": {
"CheckLineTotal": "lineTotal == round(quantity * unitPrice, 2)",
"CheckSubtotal": "subtotal == sum(lines, lineTotal)",
"CheckTaxAmount": "taxAmount == round(subtotal * taxRate, 2)",
"CheckTotal": "total == subtotal + taxAmount",
"CheckBalance": "balance == total - (amountPaid ?? 0)",
"DueDateAfterIssue": "daysBetween(issueDate, dueDate) >= 0"
},
"$format": {
"InvoiceId": "^INV-[0-9]{4}-[0-9]{4}$"
},
"$nomenclature": {
"INVOICE_STATUS": "DRAFT,SENT,PAID,OVERDUE,CANCELLED"
}
}lineTotal
must equal quantity × unitPrice (rounded to 2
decimals)subtotal must equal the
sum of all lineTotal valuestaxAmount validated
against subtotal × taxRatetotal must equal
subtotal + taxAmount?? to
default amountPaid to 0 if nulldueDate must be on or
after issueDatepaymentDate
is required only when status is 'PAID'{
"invoice": {
"invoiceId": "INV-2025-0042",
"issueDate": "2025-06-15",
"dueDate": "2025-07-15",
"customer": {
"name": "Tech Solutions Ltd",
"email": "accounts@techsolutions.com"
},
"lines": [
{
"description": "Software development",
"quantity": 40,
"unitPrice": 125.00,
"lineTotal": 5000.00
}
],
"subtotal": 5000.00,
"taxRate": 0.20,
"taxAmount": 1000.00,
"total": 6000.00,
"amountPaid": 3000.00,
"balance": 3000.00,
"status": "SENT"
}
}End of Annex C — Okyline Expression Language (Normative)