Version: 1.6.0 Date: April 2026 Status: Draft
Companion to the Okyline Core Language Quick
Reference. Covers: computed expressions
($compute), internal references
($defs/$ref), and virtual fields
($field).
Okyline includes a pure, deterministic expression
language for computed validations. It lets you express
cross-field business rules —
e.g. total == subtotal * (1 + taxRate) — that cannot be
captured by per-field constraints alone. Expressions are declared in a
$compute block and attached to fields with
(%Name).
$compute BlockDeclared at root level, alongside
$oky:
{
"$compute": {
"TaxAmount": "subtotal * taxRate",
"ValidTotal": "total == subtotal + %TaxAmount"
},
"$oky": {
"invoice": {
"subtotal": 100.0,
"taxRate": 0.2,
"total|(%ValidTotal)": 120.0
}
}
}↳ %TaxAmount references another expression;
(%ValidTotal) validates the field.
%Name.field.deep accesses members of the compute’s
result.
Rules: - Expression names: start with letter, then letters/digits/underscores - Circular dependencies → parsing error - Expressions are evaluated lazily when referenced
Parameterized computes — suffix the name with a formal parameter list; call with matching arguments:
"$compute": {
"CategoryCheck(cat)": "category != cat || rate > 0",
"InRange(lo, hi)": "it >= lo && it <= hi"
},
"$oky": {
"age|(%InRange(18, 120))": 30,
"rateS|(%CategoryCheck('S'))": 20
}↳ Params shadow sibling fields inside the body; use
this.field to unshadow. Args on a field constraint must be
literals or dotted paths; full expressions are allowed in compute
bodies. Arity mismatch = load error. Params must not collide with
special variables (it, parent,
index, …). A parameter bound to an object exposes its
members via p.field; a list parameter composes with
firstOf/at/findFirst/sum/…
(%Name)"fieldName|(%ExpressionName)": exampleValue
| Result | Validation |
|---|---|
true |
Pass |
false |
Fail — Compute error |
Non-boolean / null |
Fail — Compute error |
Constraint exclusivity: (%Name) cannot
combine with other (...) constraints (range, comparison).
Include range checks inside the expression instead.
Evaluation order: Type → Standard constraints
(@, {...}, ~...~) → Computed
expression
Container vs element compute:
"field|@ [size] (%ContainerCheck) -> (%ElementCheck)": [10, 20]
↳ Before ->: evaluated once on collection
(it = collection). After ->: evaluated per
element (it = element).
| Prefix | Resolves from |
|---|---|
| (none) | Current object |
this. |
Current object (explicit) |
parent. |
Parent of the current context |
root. |
Document root |
origin. |
Origin element (lambda only) |
prev. / next. |
Adjacent elements (lambda only) |
first. / last. |
Collection endpoints (lambda only) |
Field resolution: direct name access, dot notation
for nested (address.city), missing fields →
null, null-safe navigation.
The it variable: - In field constraint:
value of the field being validated - In aggregation lambda on scalars:
current element - In aggregation lambda on objects: current object
(rarely needed — use property names) - Outside any context:
null
| Variable | Description | Null when |
|---|---|---|
origin |
Element whose validation triggered the call | Never |
prev / next |
Adjacent elements | First / Last |
first / last |
Collection endpoints | Empty collection |
index |
0-based iteration index | Never |
size |
Collection size | Never |
| Predicate | True when |
|---|---|
isOrigin |
Current element is the origin |
isFirst |
Current element is first |
isLast |
Current element is last |
"$compute": {
"IsUnique": "countIf(parent.items, id == origin.id) == 1"
}↳ Inside the lambda: id = iteration element’s id,
origin.id = validated element’s id
| Operator | Description | Null behavior |
|---|---|---|
?? |
Null coalescing (high precedence, short-circuit) | Returns right if left is null |
+ - * / |
Arithmetic | Null propagates. Exception: string + null → treats null
as "" |
> < >=
<= |
Comparison | Returns null |
== != |
Equality | null == null → true |
=== !== |
Strict equality (IEEE-754 bit-exact) | Strict |
&& \|\| ! |
Logical | Null → false; !null → true |
? : |
Ternary | Condition null → false branch |
List literals: [a, b, c] — inline list,
usable anywhere a value is expected.
Precedence (high to low): ?? →
* / → + - →
comparisons → equality → && → || →
? :
| Function | Description | Example |
|---|---|---|
date(s, pattern?) |
Parse to LocalDate (default yyyy-MM-dd) |
date("2024-03-15") |
formatDate(d, pattern?) |
Format date to string | formatDate(d, "dd/MM/yy") →
"15/03/24" |
today() |
Current system date | today() |
daysBetween(start, end) |
Days difference | daysBetween("2024-03-15","2024-03-18") → 3 |
plusDays(d, n) |
Add days | plusDays("2024-02-28", 1) →
"2024-02-29" |
minusDays(d, n) |
Subtract days | |
plusMonths(d, n) |
Add months | plusMonths("2024-01-31", 1) →
"2024-02-29" |
minusMonths(d, n) |
Subtract months | |
plusYears(d, n) |
Add years | |
minusYears(d, n) |
Subtract years | |
year(d) month(d) day(d) |
Extract component | month("2024-03-15") → 3 |
dayOfWeek(d) |
MON=1 … SUN=7 | dayOfWeek("2024-03-15") → 5 |
dayOfYear(d) |
1–366 | |
weekOfYear(d) |
ISO week 1–53 | |
quarter(d) |
1–4 | quarter("2024-09-15") → 3 |
semester(d) |
1–2 | |
isWeekend(d) |
True if Sat/Sun | |
isLeapYear(d) |
True if leap year | |
before(d1, d2) |
d1 < d2 | |
after(d1, d2) |
d1 > d2 | |
equals(d1, d2) |
d1 == d2 |
| Function | Description | Example |
|---|---|---|
length(s) |
String length (null → 0) | length("hey") → 3 |
isEmpty(s) |
Length == 0 | |
isNullOrEmpty(s) |
Null or empty | |
isNull(v) |
v is null | |
hasValue(v) |
v is not null (opposite of isNull) | hasValue(code) → presence check |
substring(s, start, len) |
Extract substring (len = length, not end index) | substring("Hello",1,3) → "ell" |
substringBefore(s, delim) |
Before first occurrence | substringBefore("a:b:c",":") → "a" |
substringAfter(s, delim) |
After first occurrence | substringAfter("a:b:c",":") → "b:c" |
substringBeforeLast(s, d) |
Before last occurrence | |
substringAfterLast(s, d) |
After last occurrence | substringAfterLast("a.b.txt",".") →
"txt" |
replace(s, target, repl) |
Replace all | |
replaceFirst(s, t, r) |
Replace first | |
replaceLast(s, t, r) |
Replace last | |
trim(s) ltrim(s)
rtrim(s) |
Trim whitespace | |
toUpperCase(s) toLowerCase(s) |
Case conversion | |
capitalize(s) decapitalize(s) |
First char case | capitalize("hello") → "Hello" |
startsWith(s, prefix) |
Check prefix | |
endsWith(s, suffix) |
Check suffix | |
contains(s, search) |
Substring found | |
removePrefix(s, p) |
Remove prefix if present | |
removeSuffix(s, sf) |
Remove suffix if present | |
removeRange(s, start, len) |
Remove len chars from start | removeRange("Hello",1,3) → "Ho" |
padStart(s, len, ch) |
Left pad | padStart("7",3,"0") → "007" |
padEnd(s, len, ch) |
Right pad | |
repeat(times, ch) |
Repeat char | repeat(5,"*") → "*****" |
indexOf(s, sub) |
First index (-1 if not found) | |
indexOfFirst(s, sub) |
Alias for indexOf | |
indexOfLast(s, sub) |
Last index |
String index handling: negative start → clamped to
0, negative length → 0, start > length → "", end >
length → clamped. Never throws.
String ↔︎ List:
| Function | Description | Example |
|---|---|---|
chars(s) |
String → list of characters | chars("abc") → ["a","b","c"] |
split(s, sep) |
Split by literal separator | split("a,b,c", ",") → ["a","b","c"] |
join(coll, sep) |
Join list into string | join(["a","b"], "-") → "a-b" |
countIf(chars(name), in(it, ["a","e","i","o","u"])) // count vowels
join(filter(split(csv, ","), it != ""), ",") // clean CSV| Function | Description | Example |
|---|---|---|
abs(x) |
Absolute value | abs(-5) → 5 |
sqrt(x) |
Square root | sqrt(9) → 3.0 |
floor(x, scale?) |
Round down | floor(3.1415, 2) → 3.14 |
ceil(x, scale?) |
Round up | ceil(3.1415, 2) → 3.15 |
round(x, scale?, mode?) |
Round (default HALF_UP) | round(3.5, 0, "HALF_EVEN") → 4.0 |
mod(a, b) |
Remainder | mod(10, 3) → 1 |
pow(base, exp) |
Power | pow(2, 3) → 8.0 |
log(x) |
Natural log | |
log10(x) |
Base-10 log | log10(1000) → 3.0 |
toInt(v) |
Convert to integer | toInt(3.7) → 4 |
toNum(v) |
Convert to number | toNum("42") → 42.0 |
toStr(v) |
Convert to string | toStr(42) → "42" |
Rounding modes: HALF_UP (default),
HALF_DOWN, HALF_EVEN, UP,
DOWN, CEILING, FLOOR
Precision: Exact decimal arithmetic within 6 decimal
places. No IEEE-754 artifacts (0.1 + 0.2 = 0.3 exactly).
Division by zero → null.
Type promotion: int + int → int,
int + decimal → decimal, int / int → decimal
(division always returns decimal).
| Function | Object collection | Scalar collection | Description |
|---|---|---|---|
sum |
sum(coll, expr) |
sum(coll) |
Sum |
average |
average(coll, expr) |
average(coll) |
Mean |
min |
min(coll, expr) |
min(coll) |
Minimum |
max |
max(coll, expr) |
max(coll) |
Maximum |
count |
count(coll) |
count(coll) |
Non-null elements |
countAll |
countAll(coll) |
countAll(coll) |
All elements (incl. null) |
countIf |
countIf(coll, pred) |
countIf(coll, pred) |
Count where true |
exists |
exists(coll, pred) |
exists(coll, pred) |
Any match |
notExists |
notExists(coll, pred) |
notExists(coll, pred) |
No match |
sumIf |
sumIf(coll, pred, expr) |
sumIf(coll, pred, val) |
Conditional sum |
map |
map(coll, expr) |
map(coll, expr) |
Transform each |
filter |
filter(coll, pred) |
filter(coll, pred) |
Keep where true |
sum(items, quantity * unitPrice) // object collection
sum([10, 20, 30]) // scalar collection
filter([1, 2, 3, 4], it > 2) // → [3, 4]
map(items, toUpperCase(code)) // transformMembership — in:
| Form | Example |
|---|---|
| List literal | in(status, ["DRAFT", "SENT"]) |
| Nomenclature | in(status, '$INVOICE_STATUS') |
| Array field | in(code, allowedCodes) |
↳ null value → false. List as first arg → containsAll
semantics.
Lookup — lookup:
lookup(currency, rates) // get value by key from map
lookup(key, src) ?? defaultValue // with fallback
join(map(chars(code), lookup(it, charMap) ?? it), "") // char-by-char transformCompute reference — %Name:
"$compute": {
"LineTotal": "netAmount * (1 + vat)",
"InvoiceTotal": "sum(lines, %LineTotal)"
}↳ %LineTotal evaluates LineTotal for each
element in the aggregation
List element access — retrieve a single element (null-safe):
| Function | Returns |
|---|---|
firstOf(coll) |
First element, null if empty |
lastOf(coll) |
Last element, null if empty |
findFirst(coll, pred) |
First matching element, null otherwise |
findLast(coll, pred) |
Last matching element (reverse scan), null otherwise |
at(coll, idx) |
Element at idx (0-based), null if out of bounds /
negative |
Member access on any expression — chain
.field after a function call or parenthesized
expression:
firstOf(items).amount
findFirst(lines, cat == 'S').amount
%getParty('S').address.city
at(payments, 0).status↳ Dotted paths on bare identifiers (parent.x.y) are
unchanged — the postfix .field applies after )
or ]. Resolution on null base →
null.
| Context | Behavior |
|---|---|
Arithmetic (+ - *
/) |
Null propagates |
String + |
Null treated as "" |
Comparisons (< > >=
<=) |
Returns null |
Equality (==) |
null == null → true; null == x →
false |
Logical (&& \|\|) |
Null → false |
!null |
→ true |
| Division by zero | → null |
| Missing field | → null |
obj.field on null obj |
→ null (null-safe) |
$defs / $ref)Okyline supports reusable schema fragments to avoid
duplicating the same structure across a document. You define templates
once in $defs and reference them with $ref in
two modes: property-level (a field whose type comes
from a definition) or object-level (an object that
includes all fields of a template). Included structures can be adapted
with $override, $amend, or
$remove.
$defsDeclared at root level. Contains reusable schema fragments (objects or scalars):
{
"$defs": {
"Address": {
"street|@ {2,100}": "12 rue du Saule",
"city|@ {2,100}": "Lyon"
},
"Email|~$Email~ {5,100}": "user@example.com",
"Percentage|(0..100)": 50
}
}Rules: First-level entries only (no nested paths). Resolution is case-sensitive.
&Name&Address → resolves to "Address" in $defs
&Email → resolves to "Email" in $defs
↳ & = current document’s definition namespace.
Unknown name → schema rejected.
field | $ref"address | $ref @": "&Address"↳ address uses Address schema, required (@
is local)
List of referenced elements:
"addresses | $ref [1,10]": ["&Address"]↳ Array of 1-10 elements, each validated against Address
Constraint categories:
| Structural (local, at usage) | Value (included from def) |
|---|---|
@ ? [min,max] !
% label |
# {min,max} (min..max)
('A','B') ~pattern~
(%Compute) |
"primaryEmail | $ref @": "&Email",
"backupEmail | $ref ?": "&Email"↳ Same Email definition, different structural constraints per usage
{
"$oky": {
"Person": {
"$ref": "&Address",
"name|@ {2,50}": "Dupond"
}
}
}↳ All Address fields injected into Person + local name
field added
Rules: - $ref value = single reference
string (one template only) - Object-level $ref MUST target
an object definition - Object-level cycles → forbidden (detected at load
time) - Property-level recursion → allowed - Field collision → must use
$override or $amend (otherwise rejected)
$remove{
"AnonymousPerson": {
"$ref": "&Person",
"$remove": ["email", "ssn"]
}
}↳ Includes Person minus email and ssn
Rules: Array of field names. Each must exist in
template. NOT permitted when template contains conditional rules or
$compute.
$override / $amend"fieldName | $override <constraints>": <value>
"fieldName | $amend <constraints>": <value>
| Directive | Unspecified blocks | Flags (@ ? !
#) |
|---|---|---|
$override |
Removed from effective field | Only adapter’s flags kept |
$amend |
Inherited from base | Logical OR (base + adapter) |
Invariants preserved by both: field type, collection
nature, $ref target.
{
"Employee": {
"$ref": "&Person",
"name | $amend @": "John Doe",
"salary|@ (>=0)": 3000
}
}↳ Person’s name|? {1,50} + $amend @ →
effective name|@? {1,50} (added @, kept
? and {1,50})
$ref,
inject all fields$remove$override /
$amend| Error condition | Result |
|---|---|
$remove targets non-existent field |
Rejected |
$override/$amend targets non-existent
field (after removes) |
Rejected |
Both $override and $amend on same
field |
Rejected |
Type/collection change via
$override/$amend |
Rejected |
Local field collision (no
$override/$amend) |
Rejected |
| Object-level cycle | Rejected |
$remove + template with
conditionals/$compute |
Rejected |
$field)Virtual fields inject computed properties into an object
during validation, as if they were present in the JSON data —
but they are derived from existing fields, not supplied by the producer.
Typical use: compute a tier, a classification, or a flag, then use it as
a trigger in conditional directives
($appliedIf, $requiredIf, etc.). They require
$compute (Annex C).
{
"$oky": {
"order": {
"$field discountTier": "%CalculateTier",
"total": 1500.00,
"$appliedIf discountTier('GOLD')": {
"loyaltyBonus|@": 50.00
}
}
},
"$compute": {
"CalculateTier": "total >= 1000 ? 'GOLD' : 'STANDARD'"
}
}↳ discountTier is computed, exists only during
validation, used as condition trigger
Syntax:
"$field <name>": "%<ComputeName>"
%Name reference to
$compute (no inline expressions)$appliedIf
payload fields$requiredIf/$forbiddenIf field
lists (they are not data fields)$appliedIf
payloadsVirtual fields evaluate sequentially in declaration order. Each can reference previously declared ones:
"$field subtotal": "%ComputeSubtotal",
"$field tax": "%ComputeTax",
"$field total": "%ComputeTotal""ComputeSubtotal": "price * quantity",
"ComputeTax": "subtotal * 0.2",
"ComputeTotal": "subtotal + tax"↳ tax uses subtotal, total
uses both. Order matters.
Evaluation order: Parse → Virtual fields (in order) → Conditional directives → Field constraints
| Directive | Example |
|---|---|
$requiredIf |
"$requiredIf tier('PREMIUM')": ["supportEmail"] |
$requiredIfNot |
"$requiredIfNot active(true)": ["reason"] |
$forbiddenIf |
"$forbiddenIf category('RESTRICTED')": ["publicUrl"] |
$forbiddenIfNot |
"$forbiddenIfNot enabled(true)": ["legacyMode"] |
$appliedIf |
"$appliedIf type('SPECIAL')": { ... } |
$appliedIf (switch) |
"$appliedIf tier": { "('GOLD')": {...}, "('SILVER')": {...} } |
Null test:
"$requiredIf computedField(null)": ["fallback"]
$ref inclusion (must redeclare)$compute vs
$field$compute |
$field |
|
|---|---|---|
| Declared at | Root level | Object level |
| Value | Expression string | %ComputeName reference |
| Purpose | Reusable expressions | Derived values for conditions |
| Used via | (%Name) in field constraints |
Name in condition triggers |
| Scope | Global | Local to declaring object |
Okyline® is a registered trademark of Akwatype.