SKILL.md
name: okyline description: > Expert assistant for the Okyline schema language — create, edit, and convert JSON validation schemas using declarative, example-driven syntax with inline constraints. metadata: version: "2.2.0" author: Okyline repository: Okyline/Okyline-skill
Okyline Schema Language v1.7.0
Okyline is a declarative language for describing and validating JSON structures using inline constraints on field names. Schemas are valid JSON documents with real example values.
⚠️ Before any schema generation
MANDATORY: Read the reference files BEFORE producing an Okyline schema:
references/syntax-reference.md— complete syntax of constraintsreferences/internal-references.md—$defsand references (&Name)references/conditional-directives.md— if conditional logicreferences/expression-language.md— if $compute is necessaryreferences/virtual-fields.md— if $field (virtual fields) is necessaryreferences/external-imports.md— ONLY if$deps/$importare explicitly requested or already present in a loaded schema
Never generate a schema based solely on this SKILL.md file. The examples here are a summary, not an exhaustive reference.
References: when to use
Use $defs + reference (&Name) when:
- Recursion (mandatory — only way to express recursive structures)
- Repeated structures identical across multiple usages (e.g.
Period,Address) - Template pattern — base structure specialized per usage via
$overrideor$amend(e.g.Coding) - Explicit user request
Default → inline in $oky. Don't over-abstract — factorize only when it reduces real duplication or expresses a meaningful shared type.
Core Syntax
"fieldName | constraints | label": exampleValue
- fieldName: JSON field name
- constraints: Validation rules (space-separated)
- label: Optional human-readable description
- exampleValue: Determines the inferred type
If a label is present without constraints, use | |:
❌ "acheteur|Client" → "Client" parsed as constraint!
✅ "acheteur| |Client" → "Client" is the label
Minimal Schema Structure
{
"$oky": {
"name|@ {2,50}|User name": "Alice",
"email|@ ~$Email~": "[email protected]",
"age|(18..120)": 30
}
}
Essential Constraints
| Symbol | Meaning | Example |
|---|---|---|
@ |
Required field | "name|@": "Alice" |
? |
Nullable (can be null) | "middle|?": "John" |
{min,max} |
String length | "code|{5,10}": "ABC123" |
(min..max) |
Numeric range | "age|(18..65)": 30 |
('a','b') |
Enum values | "status|('ACTIVE','INACTIVE')": "ACTIVE" |
~pattern~ |
Regex or format | "email|~$Email~": "[email protected]" |
[min,max] |
Array size | "tags|[1,5]": ["eco"] |
[*] |
Array (no size constraint) | "items|[*]": [1, 2] |
-> |
Element validation constraint | "tags|[*] -> {2,10}": ["eco"] |
! |
Unique elements | "codes|[*]!": ["A","B"] |
# |
Key field (for object uniqueness) | "id|#": 123 |
&Name |
Reference to definition (value-based, type indicator) | "address": "&Address" |
Open-ended numeric constraints
When only one bound is needed, use comparison operators:
| Syntax | Meaning | Example |
|---|---|---|
(>0) |
Strictly positive | "quantity|(>0)": 5 |
(>=0) |
Positive or zero | "price|(>=0)": 29.99 |
(<100) |
Strictly < 100 | "percentage|(<100)": 50 |
(<=100) |
≤ 100 | "score|(<=100)": 85 |
❌ Never invent syntax with missing bound: (0..), (..100)
✅ Use comparisons: (>=0), (<=100)
❌ Never use huge placeholder bounds: (0..99999999), (-99999999..100)
✅ Use comparisons: (>=0), (<=100)
⚠️ INVALID SYNTAX — Okyline does NOT support open ranges:
- ❌
(0..),(..100),(1..*),(0..*) - These will cause validation errors
Unbounded collection size
Use * to indicate "no limit":
| Syntax | Meaning |
|---|---|
[*] |
Array with no size constraint |
[1,*] |
At least 1 element, no maximum |
[~pattern~:*] |
Map with validated keys, unlimited entries |
❌ Never omit a bound: [1,], [~pattern~:]
✅ Use * explicitly: [1,*], [~pattern~:*]
Built-in Formats
Use with ~$FormatName~: $Date, $DateTime, $Time, $Email, $Uri, $Uuid, $Ipv4, $Ipv6, $Hostname
Type Inference Rules
- Type is inferred from example value (no explicit declarations)
42→ Integer,3.14→ Number,"text"→ String,true→ Boolean- Arrays must have at least one element for type inference
nullcannot be used as example — no type can be inferred. Use?for nullable fields with a real example value:
❌ "middleName|?": null
✅ "middleName|?{1,50}": "Marie"
❌ "discount|?(0..100)": null
✅ "discount|?(0..100)": 15
Decimal Values Ending in .00
JSON serializers drop trailing zeros: 78.00 becomes 78.
To preserve decimal precision in examples, wrap in quotes:
❌ "amount": 78.00 → Serialized as 78, type inferred as Integer
✅ "amount": "78.00" → Preserved, type inferred as Number
This only affects decimals with zero fractional parts (.00, .0). Regular decimals (45.5) and integers (10) are unaffected.
⚠️ ? vs Absence — Critical Distinction
? and "optional field" are NOT the same thing in Okyline:
| Meaning | When to use | |
|---|---|---|
No @ |
Field may be absent from the document | FHIR optional fields, nullable in most APIs |
? |
Field may be explicitly null |
Only when null is a meaningful intentional value |
// ❌ Wrong — using ? for optional fields (common LLM mistake)
"meta|?|Resource metadata": { ... }
"subType|?|Claim subtype": { ... }
// ✅ Correct — optional = no @, never ?
"meta||Resource metadata": { ... }
"subType||Claim subtype": { ... }
// ✅ ? only when null is intentional and meaningful
"active|?|Explicitly unknown status": true
In FHIR R4 and most REST APIs, absent fields are simply omitted — they are never sent as
null. Use?only when the standard explicitly allowsnullas a distinct semantic value (e.g. "unknown" vs "not provided").
Legacy Null Tolerance
{ "$nullAsAbsentIfUndeclared": true }
When true, a null value on a field not marked ? is treated as absent instead of invalid. Use only for legacy systems that send null instead of omitting fields. Default: false.
Polymorphism vs Structural Directives — Key Distinction
These look similar but serve different purposes:
| Target | Meaning | |
|---|---|---|
$exactlyOne / $mutuallyExclusive |
Fields of an object | Constrain presence of sibling fields |
$oneOf / $anyOf |
A single field | Constrain the structure of that field's value |
// $exactlyOne — exactly one of these sibling fields must be present
"diagnosisCodeableConcept": { ... },
"diagnosisReference": { ... },
"$exactlyOne": ["diagnosisCodeableConcept", "diagnosisReference"]
// $oneOf — the payment field itself must match exactly one variant
"payment|@ $oneOf": [
{ "type|@ ('card')": "card", "cardNumber|@ {16}": "1234567812345678" },
{ "type|@ ('paypal')": "paypal", "email|@ ~$Email~": "[email protected]" }
]
Reusable Definitions — $defs
Okyline supports internal references to promote reuse and consistency.
$defs — Definition Repository
$defs is placed at root level (alongside $oky) and contains reusable schema fragments:
{
"$oky": {
"person": {
"address": "&Address",
"contact|@": "&Email"
}
},
"$defs": {
"Address": {
"street|@ {2,100}": "12 rue du Saule",
"city|@ {2,100}": "Lyon"
},
"Email|~$Email~ {5,100}": "[email protected]"
}
}
Definitions can be objects or scalars (with constraints in the key).
Reference Syntax — &Name
References use & prefix: &Address, &Email, &Person. The & at the start of a value qualifies the field as a reference — no extra modifier required.
Property-Level Reference
A field uses another schema as its type when its value is &Name:
"address|@": "&Address" // Required address
"backup|?": "&Address" // Optional, nullable
"emails|[1,5]": ["&Email"] // Array of 1-5 emails
Structural constraints (@, ?, [min,max], !) are defined locally at each usage.
Value constraints ({min,max}, (min..max), ~pattern~) are inherited from the definition.
References vs -> for arrays
The two mechanisms serve different purposes:
- Reference (
&Namevalue) = type indicator (what the elements ARE) ->= validation constraints (rules elements must satisfy) — comes AFTER the array constraint
For arrays of referenced types, the reference is the value; -> adds extra element constraints:
"items|[*]": ["&Item"] // Array of Item references
"items|[1,10]": ["&Item"] // Array of 1-10 Item references
"items|[*] -> {2,50}": ["&Item"] // With additional element constraint
Object-Level Reference — Inheritance
Include all fields from a base schema:
{
"$oky": {
"Article": {
"$ref": "&Auditable",
"title|@ {1,200}": "Mon article"
}
},
"$defs": {
"Auditable": {
"createdAt|@ ~$DateTime~": "2025-01-01T00:00:00Z",
"updatedAt|@ ~$DateTime~": "2025-01-01T00:00:00Z"
}
}
}
Modifying Inherited Fields
| Directive | Usage | Description |
|---|---|---|
$remove |
"$remove": ["field1", "field2"] |
Exclude inherited fields |
$override |
"field | $override ...": value |
Replace inherited field entirely (unspecified blocks are erased) |
$amend |
"field | $amend ...": value |
Adapt inherited field block-by-block (unspecified blocks are kept from base) |
Both $override and $amend preserve type, collection nature and reference target. In an $appliedIf branch, redefining a parent field without $override or $amend is a parsing error.
Choosing the Right Conditional Mechanism
| Need | Mechanism | Example |
|---|---|---|
| Field A required when field B has a specific value | $requiredIf |
"$requiredIf status('ACTIVE')": ["email"] |
| Field A forbidden when field B has a specific value | $forbiddenIf |
"$forbiddenIf status('CLOSED')": ["lastLogin"] |
| Field A required when field B exists/is absent | $requiredIfExist / $requiredIfNotExist |
"$requiredIfExist shipping": ["address"] |
| Different fields depending on a value | $appliedIf switch |
"$appliedIf paymentMethod": { "('CARD')": {...}, "('PAYPAL')": {...} } |
| Extra fields when a condition is met | $appliedIf simple |
"$appliedIf status('ACTIVE')": { "workDays|@": 20 } |
| Exactly one field among N must be present | $exactlyOne |
"$exactlyOne": ["email", "phone"] |
| At most one field among N | $mutuallyExclusive |
"$mutuallyExclusive": ["optionA", "optionB"] |
| At least one field among N | $atLeastOne |
"$atLeastOne": ["email", "phone", "fax"] |
| All or none of a group | $allOrNone |
"$allOrNone": ["street", "city", "zip"] |
| Condition based on a computed/derived value | $field + $appliedIf |
"$field tier": "%ComputeTier" then "$appliedIf tier('GOLD')": {...} |
| Validation depends on field's runtime type | Type Guard | "$appliedIf data(_String_)": {...} |
| Condition on null value | Null literal | "$requiredIf status(null)": ["fallback"] |
$compute vs $field
Use $compute alone to validate field values against business rules ("total|(%CheckTotal)": 120.0).
Introduce $field only when a conditional directive needs a derived value not present in the data (e.g., a computed tier, classification, or flag). If the condition can reference an existing field directly, don't use $field.
$compute Context Rules
Expressions in $compute must be attached to a field using |(%ComputeName) syntax. The expression is evaluated in the context of the object that directly contains the annotated field — all properties of that object are accessible, including sibling arrays.
Path resolution: parent always refers to the parent of the evaluation context. Context fields are accessed directly (no prefix needed), so parent is always one level up from the context. Use parent.parent to go two levels up, or root to reach the document root directly. Use this. to disambiguate when a field name collides with a reserved keyword or a compute parameter (parent, root, prev, next, first, last, origin).
Parameterized computes: a compute can declare formal parameters: "Name(p1, p2)": "body" and be called with %Name(arg1, arg2). Args on a field constraint are literals or dotted paths; full expressions are allowed from another compute body. Params shadow sibling fields inside the body — use this.field to unshadow. A param bound to an object exposes its members via p.field; a list param composes with firstOf/at/findFirst/sum/…
List element access: firstOf(list), lastOf(list), findFirst(list, pred), findLast(list, pred), at(list, idx) return a single element (null if empty / not found / out of bounds). Chain .field directly: findFirst(lines, cat == 'S').amount.
Compute reference with dotted access: %Name.field.deep accesses members of the compute's result, useful when the compute returns an object (e.g. "%DocRoot.DocumentCurrencyCode" where DocRoot = "root.Invoice ?? root.CreditNote").
See references/expression-language.md for full details and examples.
Schema Validation Checklist
Before delivering a schema, verify:
-
$okywrapper present - All required fields marked with
@ - No
?on fields that are simply optional (use?only for intentional null) - No
nullexample values — use a real example with?if nullable - No empty arrays
[]— at least one element for type inference - Decimals ending in .00 quoted as strings (
"78.00"not78.00) - Element constraints use
->(not applied directly to array field) - References use
&Nameas the field value (no|$refmodifier needed) -
$computeexpressions attached to fields with|(%Name) - No two
(...)constraint blocks on the same field - Labels use
| |Labelsyntax (not|Label) -
$iduses only letters, digits, underscores and dots - Enums use
$nomenclatureor('A','B'), not regex patterns
Reference Files
For detailed syntax and features, consult these references:
- Constraint syntax & patterns: See references/syntax-reference.md
- Conditional directives: See references/conditional-directives.md
- Expression language ($compute): See references/expression-language.md
- Internal references (
$defs,&Name): See references/internal-references.md - Virtual fields ($field): See references/virtual-fields.md
- External imports (
$deps,$import): See references/external-imports.md — load only if user explicitly requests modularization or a loaded schema already uses these
Quick Patterns
// Required email
"email|@ ~$Email~": "[email protected]"
// Required string 2-50 chars
"name|@ {2,50}": "Alice"
// Optional number range
"discount|(0..100)": 15
// Enum from nomenclature
"status|@ ($STATUS)": "ACTIVE"
// Array of unique strings, 1-10 items, each 2-20 chars
"tags|@ [1,10] -> {2,20}!": ["eco", "bio"]
// Required but nullable
"middleName|@ ?{1,50}": "Marie"
// Map with pattern keys
"translations|[~^[a-z]{2}$~:10]": {"en": "Hello", "fr": "Bonjour"}
// Reference to definition (required)
"address|@": "&Address"
// Array of referenced type
"items|@ [1,100]": ["&OrderItem"]
// Array of referenced type with element constraint
"codes|[1,10] -> {2,20}": ["&Code"]
// Object inheritance (object-level $ref key — unchanged)
"$ref": "&Auditable"
// Template pattern — object-level $ref + $override inline in array element
"coding|[*]": [{
"$ref": "&Coding",
"code|$override @ ($MARITAL_STATUS)|Code": "M"
}]
// Property-level reference (field IS the type, no specialization)
"period| |Validity period": "&Period"
// $amend — adapt inherited field keeping base constraints
"email|$amend @": "[email protected]"
// Parameterized compute — one rule, many categories
"$compute": { "RateCheck(cat)": "category != cat || rate > 0" }
"rate|(%RateCheck('S'))": 20
// List access with field chain
"$compute": { "FirstPaid": "findFirst(payments, status == 'OK').amount > 0" }
Complete Example — E-commerce Order
{
"$version": "1.0.0",
"$id": "ecommerce.order",
"$title": "E-commerce order",
"$description": "E-commerce order - Full Okyline features showcase",
"$oky": {
"orderId|# ~^ORD-[0-9]{8}$~|Order identifier": "ORD-20250107",
"createdAt|@ ~$DateTime~|Creation date": "2025-01-07T14:30:00Z",
"//createdBy|~^[A-Z]{2}[0-9]{5}$~|Created By": "PF97877",
"customer|@|Customer info": {
"id|# (>0)": 42,
"email|@ ~$Email~": "[email protected]",
"phone|? ~$Phone~|Optional phone": "+33612345678",
"type|@ ($CUSTOMER_TYPE)": "PREMIUM",
"$appliedIf type": {
"('PREMIUM')": {
"loyaltyPoints|@ (>=0)": 1500,
"discountRate|@ (0..30)": 15
},
"('BUSINESS')": {
"companyName|@ {2,100}": "Acme Corp",
"vatNumber|@ ~$VatNumber~": "FR12345678901"
}
}
},
"shipping|@|Shipping address": "&Address",
"billing|?|Billing if different": "&Address",
"lines|@ [1,50] -> !|Order lines": [
{
"sku|# ~$Sku~": "PRD-00123",
"name|@ {2,200}": "Wireless Headphones",
"quantity|@ (1..999)": 2,
"unitPrice|@ (>0)": 79.99,
"lineTotal|(%LineTotal)": 159.98,
"category|($CATEGORY)": "ELECTRONICS"
}
],
"payment|@": {
"method|@ ($PAYMENT_METHOD)": "CARD",
"status|@ ($PAYMENT_STATUS)": "PAID",
"$requiredIf status('PAID')": ["paidAt", "transactionId"],
"$forbiddenIf status('PENDING')": ["paidAt", "transactionId"],
"paidAt|~$DateTime~": "2025-01-07T14:32:00Z",
"transactionId|~$TransactionId~": "TXN-A1B2C3D4E5F6"
},
"amounts|@|Amounts": {
"subtotal|@ (%ValidSubtotal)": 159.98,
"shippingCost|@ (>=0)": 5.99,
"discount|@ (>=0)": 24.0,
"tax|@ (>=0)": 28.39,
"total|@ (%ValidTotal)": 170.36
},
"status|@ ($ORDER_STATUS)": "CONFIRMED",
"tags|? [0,10] -> {1,30}!|Optional tags": ["gift", "express"],
"notes|? {0,500}|Internal notes": "Handle with care"
},
"$defs": {
"Address": {
"street|@ {5,200}": "123 Main Street",
"city|@ {2,100}": "Paris",
"postalCode|@ ~^[0-9]{5}$~": "75001",
"country|@ ~^[A-Z]{2}$~": "FR"
}
},
"$format": {
"Phone": "^\\+[0-9]{11,14}$",
"VatNumber": "^[A-Z]{2}[0-9]{9,12}$",
"TransactionId": "^TXN-[A-Z0-9]{12}$",
"Sku": "^[A-Z]{3}-[0-9]{5}$"
},
"$nomenclature": {
"CUSTOMER_TYPE": "STANDARD, PREMIUM, BUSINESS",
"CATEGORY": "ELECTRONICS, CLOTHING, HOME, FOOD, OTHER",
"PAYMENT_METHOD": "CARD, PAYPAL, TRANSFER, CRYPTO",
"PAYMENT_STATUS": "PENDING, PAID, FAILED, REFUNDED",
"ORDER_STATUS": "DRAFT, CONFIRMED, SHIPPED, DELIVERED, CANCELLED"
},
"$compute": {
"LineTotal": "lineTotal == quantity * unitPrice",
"ValidSubtotal": "subtotal > 0 && subtotal == sum(parent.lines, lineTotal)",
"ValidTotal": "total > 0 && total == subtotal + shippingCost + tax - discount"
}
}
Document Metadata (Optional) - Must be generated in this order
{
"$okylineVersion": "1.7.0",
"$version": "1.0.0",
"$id": "my-schema",
"$state": "DRAFT",
"$title": "My Schema",
"$description": "Schema description",
"$additionalProperties": false,
"$sequence": false,
"$decimalScale": 6,
"$oky": {
...
},
"$entries": { "Create": "&Item", "Update": "&ItemPatch" },
"$defs": { ... },
"$format": { "Code": "^[A-Z]{3}-\\d{4}$" },
"$compute": { "Total": "price * quantity" },
"$nomenclature": { "STATUS": "ACTIVE,INACTIVE" }
}
$idformat: only letters, digits, underscores and dots. Pattern:^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)*$❌"personne-vehicules"(hyphen not allowed) ✅"personne.vehicules"or"personne_vehicules"$version: Semver 2.0.0 ("1.0.0","2.0.0-rc.1"). Pure metadata unless the schema is published or imported.$state:"DRAFT"(default, mutable) or"FINAL"(frozen, immutable). Omit unless lifecycle is meaningful.$decimalScale: root-level only. Decimal precision for numeric comparisons, ranges and computes (default6, max18).$entries: extra validation entry points within one schema; values are local&Namereferences. Default target stays$okywhen no entry is supplied. Only declare when consumers need to validate distinct payload shapes from the same contract.
Structural Group Directives
Constrain which fields from a group must be present. No condition required.
"$atLeastOne": ["email", "phone"] // at least one present
"$mutuallyExclusive": ["deceasedBoolean", "deceasedDateTime"] // at most one
"$exactlyOne": ["diagnosisCode", "diagnosisRef"] // exactly one
"$allOrNone": ["street", "city", "zip"] // all or none
// Multiple independent groups → use suffix
"$mutuallyExclusive_deceased": ["deceasedBoolean", "deceasedDateTime"],
"$mutuallyExclusive_birth": ["multipleBirthBoolean", "multipleBirthInteger"]
See references/conditional-directives.md for full details.
$compute — Cross-Collection Pattern
To validate a constraint across an array from root context, attach the compute to the array field itself:
"insurance|@ [1,*] -> ! (%FocalInsurance)": [{
"focal|@": true, ...
}],
"$compute": {
"FocalInsurance": "countIf(insurance, focal) == 1"
}
countIf(collection, expr)— counts elements where expr is truthy- For boolean fields:
countIf(insurance, focal)countstruevalues - For other fields:
countIf(items, status == 'ACTIVE') - Always attach the compute to the most semantically relevant field
Common Mistakes to Avoid
- Missing
$okywrapper - Empty arrays (need at least one element)
- Confusing
[1,10](array size) with-> {1,10}(element constraint) - Forgetting to escape backslashes in regex (
\d→\\d) - Defining
$computeexpressions without attaching them to fields with|(%Name) - Using absolute paths in
$computeinstead of relative references to parent context - Reference value without the
&prefix (correct:"&Address", wrong:"Address"— without&it would be a plain string example) - Object-level
$reftargeting a scalar definition (must target object schema) - Redefining an inherited field without using
$overrideor$amend - Creating circular object-level references (A includes B includes A)
- Placing
$defsinside$oky(must be at root level) - Using empty brackets
[]for arrays — use[*]or omit size constraint entirely - Applying element constraints (enum, length, range) directly to array field instead of using
->: ❌"permis|@ ('A','B','C')[]": ["B"]✅"permis|@ -> ('A','B','C')": ["B"]✅"permis|@ [1,5] -> ('A','B','C')": ["B"] - Using hyphens in
$id— only letters, digits, underscores and dots are allowed - Misplacing the reference for arrays of references — the
&Nameis the value, not a constraint to put after->: ❌"children|[*] -> &Node": []✅"children|[*]": ["&Node"]✅"children|[1,10] -> {2,50}": ["&Node"] - Using open-ended range syntax
(0..)or(..100)— these don't exist! ❌"price|(0..)": 29.99✅"price|(>=0)": 29.99 - Using multiple value constraint blocks
(...)on the same field: ❌"montantTTC|@ (>=0) (%LigneTTC)": 4320✅"montantTTC|@ (%LigneTTC)": 4320 - Using null as an example value, even with ? Okyline needs a valid example to infer the type.:
❌
"name|? ": null✅"name|? ": "Charles" - Using decimal number example ending in .00 without quotes.
❌
"amount|? ": 800.00✅"amount|? ": "800.00"
When a field uses $compute, ALL value constraints must be inside the compute expression:
"$compute": {
"LigneTTC": "montantTTC >= 0 && montantTTC == montantNetHT + montantTVA"
}
Silent errors (no syntax error, but validation problem)
- Decimals ending in
.00→ use"150.00"(otherwise inferred as Integer) - Example
null→ cannot infer type - Empty array
[]→ cannot infer element type -
"field|Label"→ "Label" parsed as constraint (use"field| |Label")