Your First Steps into Example-Driven Schema Design
Okyline is a revolutionary approach to JSON schema design. Instead of wrestling with complex schema languages, you simply write a JSON example and add inline constraints. It's that simple.
field name | constraints | label
This simple pattern is all you need: the field name, followed by optional validation constraints and label, then the example value. Everything inline, right where you need it.
Progressive approach: Your example JSON is already a valid Okyline schema that validates structure and data types. Add constraints progressively as your validation needs grow.
Your schema is a real JSON document with actual example values. What you see is what you validate.
No need to declare types explicitly. Okyline infers them from your example values automatically.
Start simple, add constraints as needed. You don't have to define everything upfront.
Convert your Okyline schemas to standard JSON Schema whenever you need to.
Let's jump right in with a complete example:
{
"$oky": {
"username|@ {3,20}|User login name": "alice",
"email|@ ~$Email~|Email address": "alice.vannier@example.com",
"age|@ (18..120)|Age in years": 25,
"role|('USER','ADMIN')|User role": "USER",
"verified|?|Email verification status": true
}
}
| Field Name | Constraints | Label | Meaning |
|---|---|---|---|
username |
@ {3,20} |
User login name | Required string between 3 and 20 characters |
email |
@ ~$Email~ |
Primary email address | Required string matching email format |
age |
@ (18..120) |
Age in years | Required number between 18 and 120 |
role |
('USER','ADMIN') |
User role | Optional enumeration with two possible values |
verified |
? |
Email verification status | Optional field, value can be null if present |
All Okyline schemas must start with a $oky root object. This identifies the document as an Okyline schema.
Okyline uses a simple symbol-based syntax for validation rules. Master these 9 core symbols and you're ready to design:
| Constraint | Example | Validation Rule |
|---|---|---|
@ Required |
"name|@": "Alice" |
Field must be present |
? Nullable |
"middleName|?": "Marie"" |
Optional field, value can be null if present |
{min,max} Length |
"username|{3,20}": "Alice" |
String must be 3-20 characters |
(min..max) Range |
"age|(18..120)": 25 |
Number must be between 18 and 120 |
(1,5,9,12) Enum |
"status|(1,9)" |
Must be one of the listed values |
('A','B') Enum |
"status|('ACTIVE','INACTIVE')" |
Must be one of the listed values |
~format~ Pattern |
"email|~$Email~" |
Must match the format or regex |
[min,max] Array |
"tags|[1,10]": ["tech"] |
Array must have 1-10 elements |
# Key |
"id|#": 12345 |
Marks field as unique identifier |
! Unique |
"emails|[*]!": ["alice@example.com"] |
All array elements must be unique |
-> Items |
"codes|[*]->{2,5}" |
Each array element: 2-5 characters |
Constraints can be combined for powerful validation rules:
{
"$oky": {
"productId|@ #": 12345,
"name|@ {1,100}": "Laptop",
"price|@ (>=0)": 999.99,
"stock|@ (10..1000)": 42,
"category|@ ('ELECTRONICS','BOOKS','CLOTHING')": "ELECTRONICS",
"tags|[1,10]->{2,20}!": ["tech", "computers"]
}
}
Okyline naturally supports nested structures:
{
"$oky": {
"user|@": {
"name|@ {2,100}": "Alice Smith",
"email|@ ~$Email~": "alice@example.com",
"address|@": {
"street|@ {5,200}": "123 Main St",
"city|@ {2,100}": "Springfield",
"country|@ {2,2}": "US",
"postalCode|~^[0-9]{5}$~": "12345"
}
}
}
}
Okyline treats collections homogeneously, whether they are Arrays (lists with numeric indices) or Maps (objects with dynamic string keys). Both use similar constraint syntax with the -> operator.
Arrays use [size_constraint] followed by element constraints:
"tags|@ [1,10] -> {2,20}!": ["eco", "bio", "local"]
[1,10] = array size between 1 and 10-> {2,20} = each element is a string of 2-20 characters! = all elements must be unique
Maps use [key_pattern:size_constraint] followed by value constraints:
"metadata|[*:20] -> {1,100}": {
"author": "Alice",
"version": "1.2.3",
"env": "production"
}
[*:20] = any string keys, maximum 20 entries-> {1,100} = each value is a string of 1-100 characters
"translations|@ [~^[a-z]{2}$~:10] -> {1,100}": {
"en": "Hello",
"fr": "Bonjour",
"es": "Hola"
}
[~^[a-z]{2}$~:10] = keys must match regex (2 lowercase letters), max 10 entries-> {1,100} = each value is a string of 1-100 characters
"products|[~^SKU-\\d{5}$~:*]": {
"SKU-12345": {
"name|@": "Product A",
"price|@ (0..1000)": 29.99
}
}
[~^SKU-\\d{5}$~:*] = keys match "SKU-" + 5 digits, unlimited entriesArrays: [size] controls the number of elements
Maps: [key_pattern:size] controls key format and number of entries
Both: Use -> to apply constraints to elements/values
Okyline provides powerful conditional validation mechanisms. Conditions can be based on field values or field existence. There are three main types of conditional constraints:
Add fields dynamically based on conditions. Supports $else for alternative structures.
| Directive | Condition Type | Meaning |
|---|---|---|
$appliedIf / $appliedIfNot |
Field value | Add structure when field value matches / doesn't match condition |
$appliedIfExist / $appliedIfNotExist |
Field existence | Add structure when field exists / doesn't exist |
{
"$oky": {
"userType|@ ('INDIVIDUAL','COMPANY')": "COMPANY",
"$appliedIf userType('INDIVIDUAL')": {
"firstName|@ {2,50}": "John",
"lastName|@ {2,50}": "Doe",
"$else": {
"companyName|@ {2,100}": "Acme Corp",
"taxId|@ {9,20}": "123456789"
}
}
}
}
{
"$oky": {
"tracking": "ABC123",
"$appliedIfExist tracking": {
"carrier|@": "DHL",
"estimatedDelivery|@~$Date~": "2025-12-25"
},
"$appliedIfNotExist email": {
"phone|@": "+33612345678",
"phoneVerified|@": true
}
}
}
{
"$oky": {
"paymentMethod|@ ('CARD','BANK','CASH')": "CARD",
"$appliedIf paymentMethod": {
"('CARD')": {
"cardNumber|@ {16}": "1234567812345678",
"cvv|@ {3}": "123",
"expiryDate|@ ~$Date~": "2026-12-31"
},
"('BANK')": {
"iban|@ {15,34}": "FR7612345678901234567890123",
"bic|@ {8,11}": "BNPAFRPP"
},
"('CASH')": {
"receiptNumber|@": "RCP-2025-001"
},
"$else": {
"note|@": "Unknown payment method"
}
}
}
}
('value') branches based on a single field. Each value gets its own structure.
Much cleaner than nested if/else.
Make specific fields required based on conditions.
| Directive | Condition Type | Meaning |
|---|---|---|
$requiredIf / $requiredIfNot |
Field value | Fields required when value matches / doesn't match condition |
$requiredIfExist / $requiredIfNotExist |
Field existence | Fields required when another field exists / doesn't exist |
{
"$oky": {
"accountType|@ ('PERSONAL','BUSINESS')": "BUSINESS",
"email": "aurelien.barrot@example.com",
"businessLicense": "BL-12345",
"$requiredIf accountType('BUSINESS')": ["businessLicense"],
"$requiredIfNot accountType('BUSINESS')": ["email"]
}
}
{
"$oky": {
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"phone": "+33612345678",
"$requiredIfExist firstName": ["lastName"],
"$requiredIfNotExist email": ["phone"]
}
}
Prevent specific fields from being present based on conditions.
| Directive | Condition Type | Meaning |
|---|---|---|
$forbiddenIf / $forbiddenIfNot |
Field value | Fields forbidden when value matches / doesn't match condition |
$forbiddenIfExist / $forbiddenIfNotExist |
Field existence | Fields forbidden when another field exists / doesn't exist |
{
"$oky": {
"accountStatus|@ ('ACTIVE','SUSPENDED','CLOSED')": "CLOSED",
"lastLogin|~$DateTime~": "2025-01-10T15:30:00Z",
"closureReason|{10,500}": "User requested deletion",
"$forbiddenIf accountStatus('CLOSED')": ["lastLogin"],
"$forbiddenIfNot accountStatus('CLOSED')": ["closureReason"]
}
}
{
"$oky": {
"archived": true,
"sku": "SKU-12345",
"internalCode": "INT-999",
"$forbiddenIfExist archived": ["active"],
"$forbiddenIfNotExist sku": ["internalCode"]
}
}
You can combine multiple conditional directives in the same schema:
{
"$oky": {
"employeeStatus|@ ('ACTIVE','ON_LEAVE','TERMINATED')": "TERMINATED",
"workDays|(1..22)": 20,
"leaveReason|{10,200}": "Parental leave",
"terminationDate|~$Date~": "2025-12-31",
"email": "lea.legall@example.com",
"$requiredIf employeeStatus('ACTIVE')": ["workDays"],
"$requiredIfExist leaveReason": ["employeeStatus"],
"$forbiddenIfNot employeeStatus('TERMINATED')": ["terminationDate"],
"$forbiddenIfNotExist email": ["phone"]
}
}
Value-based conditions: Test field values with If/IfNot (e.g., status('TERMINATED'))
Existence-based conditions: Test field presence with IfExist/IfNotExist
Three constraint types: $appliedIf (structure), $requiredIf (mandatory), $forbiddenIf (prohibited)
Okyline provides three powerful directives that start with $ to add advanced capabilities to your schemas:
Define centralized lists of allowed values (enumerations) that can be referenced throughout your schema:
{
"$oky": {
"orderId|@ #": 12345,
"status|@ ($STATUS)": "ACTIVE",
"shippingCountry|@ ($COUNTRIES)": "FRA",
"billingCountry|($COUNTRIES)": "DEU",
"currency|@ ($CURRENCIES)": "EUR"
},
"$nomenclature": {
"STATUS": "DRAFT,VALIDATED,REJECTED,ACTIVE,INACTIVE,ARCHIVED",
"COUNTRIES": "FRA,DEU,ESP,USA,GBR,ITA",
"CURRENCIES": "USD,EUR,GBP,JPY,CHF"
}
}
$nomenclature|($NAME) in field constraintsYou can use $format, $nomenclature, and $compute together in the same schema for maximum power and maintainability!
Define reusable format validators that can be referenced throughout your schema:
{
"$oky": {
"productCode|@ ~$ProductCode~": "AB-1234",
"sku|@ ~$SKU~": "SKU-00012345",
"relatedProducts|[*]->~$ProductCode~": [
"CD-5678",
"EF-9012"
]
},
"$format": {
"ProductCode": "^[A-Z]{2}-[0-9]{4}$",
"SKU": "^SKU-[0-9]{8}$"
}
}
$format block~$FormatName~ anywhere in the schemaDefine expressions that compute values from other fields. Use these computed values in validation constraints:
{
"$oky": {
"name": "Lea",
"total|@ (%CheckTotal)": 120.0,
"items|@ [1,100]": [
{
"quantity|@ (1..100)": 2,
"unitPrice|@ (>0)": 50.0,
"tax|@ (0.05,0.1,0.5,0.2)": 0.2,
"amount|@ (%CheckLineAmount)": 120.0
}
]
},
"$compute": {
"LineAmount": "quantity * round((unitPrice * (1 + tax)),2)",
"CheckLineAmount": "amount == %LineAmount",
"CheckTotal": "total == sum(items, amount)"
}
}
|@ (%CheckTotal)%ComputeNameEvaluation Context: When a compute expression is evaluated, it has access to all fields in the parent object where the validation is triggered.
For example, in the invoice above, the CheckLineAmount expression can access amount, quantity, unitPrice and tax because they are in the same object (an item).
When one expression uses another expression, it passes its context to it.
In this example, %LineAmount, referenced by CheckLineAmount, therefore also has access to quantity, unitPrice and tax
Two Usage Modes:
"LineAmount": "quantity * round((unitPrice * (1 + tax)),2)" → computes the total line with tax"amount|@ (%CheckTotal)" → validates that CheckTotal returns trueHow Validation Works: When you write "amount|@ (%CheckAmount)", Okyline evaluates the CheckAmount expression and checks that it returns true. The expression must be a boolean condition. For example:
CheckTotal should be: "total == sum(items, amount)"Key Point: The compute expression must include the comparison (==, >, <, etc.) to return true or false.
Don't try to write the perfect schema on the first try. Start with a basic example and add constraints as you discover validation needs:
{"$oky": {...}}@ to required fieldsOkyline supports inline documentation after the constraints:
{
"$oky": {
"email|@ ~$Email~|User's primary email address": "lea.bocase@example.com",
"age|@ (18..120)|Age in years": 25
}
}
Don't reinvent the wheel. Okyline provides some built-in format validators:
~$Date~ for ISO 8601 dates (YYYY-MM-DD)~$DateTime~ for ISO 8601 timestamps~$Time~ for ISO 8601 times~$Email~ for email addresses~$Uuid~,~$Uri~,~$IPv4~,~$IPv6~,$Hostname~{"$oky": {...}}[1,10] is size, [*]->{2,5} is element constraint| Directive | Purpose | Example |
|---|---|---|
$oky |
Root wrapper (required) | {"$oky": {...}} |
$nomenclature |
Reusable value lists (enums) | "$nomenclature": {"STATUS": "ACTIVE,INACTIVE"} |
$format |
Define reusable formats | "$format": {"SKU": "^[A-Z]{3}-[0-9]{4}$"} |
$compute |
Define calculated values | "$compute": {"Total": "price * qty"} |
| Directive | Purpose | Example |
|---|---|---|
$appliedIf / $appliedIfNot |
Conditional structure (value-based, supports $else). Switch-Case Mode also supported with $appliedIf | "$appliedIf status('ACTIVE')": {...} |
$appliedIfExist / $appliedIfNotExist |
Conditional structure (existence-based) | "$appliedIfExist tracking": {...} |
$requiredIf / $requiredIfNot |
Required fields (value-based) | "$requiredIf type('BUSINESS')": ["taxId"] |
$requiredIfExist / $requiredIfNotExist |
Required fields (existence-based) | "$requiredIfExist firstName": ["lastName"] |
$forbiddenIf / $forbiddenIfNot |
Forbidden fields (value-based) | "$forbiddenIf status('CLOSED')": ["login"] |
$forbiddenIfExist / $forbiddenIfNotExist |
Forbidden fields (existence-based) | "$forbiddenIfExist archived": ["active"] |
"name|@ {2,50}": "Alice Smith"
"status|('DRAFT','PUBLISHED','ARCHIVED')": "DRAFT"
"status|(1,2,5,10)":5
"amount|@ (20..50)": 7500.60
"email|@ ~$Email~": "sophie.riberro@example.com"
"createdAt|@ ~$Date~": "2025-01-15"
"productIds|@ [1,*] -> (1..1000) ! ": [101, 102, 103]
"middleName|@ ?{1,50}": "Marie"
"items|@ [1,*] -> !": [
{
"sku|@ #": "SKU-001",
"name|@": "Product A"
},
{
"sku|@ #": "SKU-002",
"name|@": "Product B"
}
]
"translations|@ [~^[a-z]{2}$~:10] -> {1,100}": {
"en": "Hello",
"fr": "Bonjour",
"es": "Hola"
}
"code|~$Code~": "ABC-1234"
"$format": {
"Code": "^[A-Z]{3}-[0-9]{4}$"
}
"country|($COUNTRY)": "FRA"
"$nomenclature": {
"COUNTRY": "FRA,DEU,ESP,USA,GBR,ITA"
}
"unitPrice|@ (>=0)": 100.50,
"quantity|@ (1..500)": 3,
"amount|@ (%CheckAmount)": 301.50
"$compute": {
"CheckAmount": "amount == unitPrice * quantity"
}
{
"userType|@ ('INDIVIDUAL','COMPANY')": "COMPANY",
"$appliedIf userType('COMPANY')": {
"companyName|@ {2,100}": "Acme Corp"
}
}
Okyline's compute expressions support a rich set of functions for data manipulation and validation. All functions are null-safe and deterministic (no side effects).
All functions handle null values gracefully without throwing exceptions. Arithmetic operations propagate null (except string concatenation which treats null as "").
Use the ?? operator for null coalescing: price ?? 0 (price will be equal to 0 if null)
isNullOrEmpty(s)isEmpty(s)substring(s,start,len)substringBefore(s,delim)substringAfter(s,delim)replace(s,target,repl)trim(s)length(s)startsWith(s,prefix)endsWith(s,suffix)contains(s,search)toUpperCase(s)toLowerCase(s)capitalize(s)decapitalize(s)padStart(s,len,ch)padEnd(s,len,ch)repeat(times,ch)indexOf(s,sub)indexOfLast(s,sub)
abs(x)sqrt(x)floor(x,scale?)ceil(x,scale?)round(x,scale?,mode?)mod(a,b)pow(base,exp)log(x)log10(x)random(min?,max?)toInt(v)toNum(v)
date(dateString,pattern?)formatDate(date,pattern?)today()daysBetween(start,end)plusDays(date,days)minusDays(date,days)plusMonths(date,months)minusMonths(date,months)plusYears(date,years)minusYears(date,years)isWeekend(date)isLeapYear(date)year(date)month(date)day(date)
sum(collection,expr)average(collection,expr)min(collection,expr)max(collection,expr)count(collection)countAll(collection)countIf(collection,expr)
For detailed documentation on each function including parameters, examples, and behavior, refer to the Okyline Language Specification v1.0.0 - Annex C.
Okyline schemas can include optional metadata fields at the root level to document and version your schemas:
| Metadata Field | Purpose | Example |
|---|---|---|
$okylineVersion |
Version of Okyline specification | "1.0.0" |
$id |
Unique identifier for your Okyline scheme within your organization | "ecommerce.order" |
$version |
Version of your schema | "1.0.4" |
$title |
Human-readable schema title | "User Profile Schema" |
$description |
Description of what the schema validates | "Schema for user profiles" |
$additionalProperties |
Allow unknown fields in validated data | false (default) |
{
"$version": "1.0.4",
"$id": "ecommerce.order",
"$title": "E-commerce Order Schema",
"$description": "Schema for validating customer orders",
"$additionalProperties": false,
"$oky": {
"orderId|@ ~$OrderId~": "ORD-12345678",
"status|@ ($STATUS)": "PENDING",
"customerEmail|@ ~$Email~": "purchasing@acme.com",
"total|@ (0..1000)": 99.99
},
"$nomenclature": {
"STATUS": "PENDING,CONFIRMED,SHIPPED,DELIVERED,CANCELLED"
},
"$format": {
"OrderId": "^ORD-[0-9]{8}$"
}
}
The $additionalProperties field controls whether extra fields not defined in the schema are allowed:
$additionalProperties can be defined at the root level (applies globally) or within specific objects (applies only to that object). Child objects inherit the root setting unless they override it locally.
{
"$additionalProperties": false,
"$oky": {
"user|@": {
"name|@": "Alice",
"email|@ ~$Email~": "alice.royer@example.com"
},
"origin": {
"$additionalProperties": true,
"source": "web",
"timestamp": "2025-01-15T10:30:00Z"
}
}
}
Always include $version, $title, and $id in production schemas to improve maintainability and documentation. Use semantic versioning for $version (e.g., "1.2.3").
Let's put it all together with a realistic API request/response schema:
{
"$version": "2.1.0",
"$id": "ecommerce.order",
"$title": "Order Schema",
"$description": "Schema for e-commerce orders",
"$oky": {
"order": {
"orderId|@ #~$OrderId~|Unique order identifier": "ORD-12345678",
"customerId|@ (>0)|Customer ID": 42,
"orderDate|@ ~$DateTime~|Order timestamp": "2025-01-15T10:30:00Z",
"status|@ ($ORDER_STATUS)|Order status": "PENDING",
"items|@ [1,100] -> !|Order items": [
{
"sku|@ #~$Sku~|Product SKU": "SKU-ABC12345",
"name|@ {2,200}|Product name": "Wireless Mouse",
"quantity|@ (1..1000)|Quantity": 2,
"vat|(0.05,0.1,0.15,0.2)":0.2,
"unitPrice|@ (>0)|Unit price": 50.0,
"netAmount|(%CheckNetAmount)":100.0,
"grossAmount|(%CheckGrossAmount)":120.00
}
],
"paymentMethod|@ ($PAYMENT_METHOD)|Payment method": "CARD",
"subTotal|(%CheckSubtotal)|Total net amount":100.0,
"total|@ (%CheckTotal)|Total gross amount": 120.00,
"$requiredIf status('SHIPPED','DELIVERED')": ["trackingNumber"]
}
},
"$nomenclature": {
"ORDER_STATUS": "PENDING,CONFIRMED,SHIPPED,DELIVERED,CANCELLED",
"PAYMENT_METHOD": "CARD,PAYPAL,BANK_TRANSFER"
},
"$format": {
"OrderId": "^ORD-[0-9]{8}$",
"Sku": "^SKU-[A-Z]{3}[0-9]{5}$"
},
"$compute": {
"CheckNetAmount": "netAmount == round(unitPrice * quantity,2)",
"CheckGrossAmount": "grossAmount == round(netAmount * (1 + vat),2)",
"CheckSubtotal": "subTotal == sum(items,netAmount)",
"CheckTotal": "total == sum(items,grossAmount)"
}
}
$version, $id, $title, $description for schema documentationorderId|@ #~$OrderId~ and sku|@ #~$Sku~ with regex patterns defined in $formatstatus|@ ($ORDER_STATUS) and paymentMethod|@ ($PAYMENT_METHOD) from $nomenclature# marker on orderId and sku for unique identifiersitems|@ [1,100] -> ! requires 1-100 unique itemsnetAmount|(%CheckNetAmount), grossAmount|(%CheckGrossAmount), subTotal|(%CheckSubtotal) validate calculations$requiredIf status('SHIPPED','DELIVERED') makes trackingNumber required for certain statusescustomerId|@ (>0), quantity|@ (1..1000), total|@ (>0)The best way to learn Okyline is to experiment with it. Visit the free online editor:
https://community.studio.okyline.io
Features live validation, JSON Schema export, and interactive documentation.
Add the Okyline library to your Java or Kotlin project via Maven Central. The library has zero external dependencies, making it lightweight and easy to integrate.
<dependency>
<groupId>io.akwatype</groupId>
<artifactId>okyline</artifactId>
<version>1.0.1</version>
</dependency>
implementation("io.akwatype:okyline:1.0.1")
import io.akwatype.okyline.api.OkylineApi;
import java.util.List;
public class TestOkyline {
public static void main(String[] args) {
String schema = """
{
"$oky": {
"user": {
"name|@ {2,50}": "John Doe",
"age|(18..120)": 30,
"email|~$Email~": "john@example.com"
}
}
}
""";
String jsonToValidate = """
{ "user": {
"name": "Alice", "age": 25, "email": "alice@test.com"
} }
""";
// --------------------- Load and compile schema
OkylineApi okyline = new OkylineApi();
List<String> schemaErrors = okyline.loadSchema(schema);
if (!schemaErrors.isEmpty()) {
schemaErrors.forEach(System.err::println);
return;
}
System.out.println("Schema loaded successfully!");
// --------------------- Validate JSON
List<String> errors = okyline.validate(jsonToValidate);
if (errors.isEmpty()) {
System.out.println("Valid JSON: OK");
} else {
errors.forEach(System.err::println);
}
} }
{
"user": {
"name": "A",
"age": 125,
"email": "alice@te@st.com"
}
}
The string at 'user.name' is too short: 1 < 2 Value: 125 at: 'user.age' does not match (18..120) The string 'alice@te@st.com' at 'user.email'
does not match okyline built-in validation '$Email'
You now have everything you need to start designing Okyline schemas. Remember: start with an example, add constraints progressively, and validate as you go. Happy schema designing!
Okyline® and Akwatype® are registered trademarks of Akwatype.
Document Information
Getting Started with Okyline - Your First Steps
Version 1.0.0 | November 2025
Based on Okyline Language Specification v1.0.0
License
This work is licensed under Creative Commons Attribution-NoDerivatives 4.0 International (CC BY-ND 4.0).
You are free to share this guide in any medium or format for any purpose, even commercially, under the following terms:
Trademark Notice
The names "Okyline" and "Akwatype" and their respective logos are trademarks of Akwatype. Use of these trademarks is subject to Akwatype's trademark policy.