Okyline Free Java Library - User Guide

Library version: 1.9.0  ·  Okyline spec: 1.8.0 Date: September 2026 Status: Draft

The Okyline free Java library validates JSON documents against Okyline schemas - the same example-driven schemas you write by hand or in Studio. It is a single, zero-dependency JAR for Java and Kotlin.

Versions. The library version (1.9.0) and the Okyline language specification it implements (1.8.0) move independently - see Versions below.

Install

Maven

<dependency>
    <groupId>io.akwatype</groupId>
    <artifactId>okyline</artifactId>
    <version>1.9.0</version>
</dependency>

Gradle

implementation("io.akwatype:okyline:1.9.0")

Quick start

Load a schema once, then validate JSON documents against it. Both calls return a List<String> of error messages - an empty list means success.

Take this schema (typed fields, a custom format and a nomenclature):

{
  "$oky": {
    "name|@ {2,50}|Full name": "John Doe",
    "employeeId|@ ~$EmployeeID~|Employee ID": "AD-00124",
    "role|@ ($ROLES)|User role": "USER"
  },
  "$format": {
    "EmployeeID": "^[A-Z]{2}-\\d{5}$"
  },
  "$nomenclature": {
    "ROLES": "ADMIN,USER,GUEST"
  }
}

and this document:

{ "name": "John Doe", "employeeId": "AD-00124", "role": "USER" }
import io.akwatype.okyline.api.OkylineApi;
import java.util.List;

OkylineApi okyline = new OkylineApi();

List<String> schemaErrors = okyline.loadSchema(schema);   // schema = the JSON above
if (!schemaErrors.isEmpty()) {
    schemaErrors.forEach(System.err::println);            // the schema did not compile
    return;
}

List<String> errors = okyline.validate(json);             // json = the document above
if (errors.isEmpty()) {
    System.out.println("Valid");
} else {
    errors.forEach(System.err::println);
}

loadSchema compiles the schema and returns any compilation errors. validate checks a document and returns the validation errors. An empty list means the document conforms.

Validating against named entries ($entries)

A schema can expose several validation targets through $entries - useful when one contract defines multiple payload shapes. Validate against an entry by name with validate(json, entryName); the no-argument validate(json) validates against the root ($oky).

{
  "$title": "Company",
  "$entries": {
    "Address": "&Address",
    "AddressList": "&AddressList"
  },
  "$oky": {
    "company|@|Company identification": {
      "corporateName|@ {100}|Name of the company": "ACME",
      "externalIdentifier|@ {5,12}|External identifier": "165987456",
      "address||Address of the company": "&Address"
    }
  },
  "$defs": {
    "Address": {
      "type|@ ('REGISTRATION','BILLING','POSTAL')": "BILLING",
      "street|{100}|Street": "144 avenue des marronniers",
      "zipCode|{10}|Zip code": "99152",
      "city|{100}|City": "Belleville",
      "country|@ {50}": "France"
    },
    "AddressList|[*]": ["&Address"]
  }
}
okyline.loadSchema(companySchema);

// Default target: the root document ($oky), which holds "company"
okyline.validate(companyJson);

// Named entry "Address": a single address object
List<String> e1 = okyline.validate("""
    {
      "type": "BILLING",
      "street": "144 avenue des marronniers",
      "zipCode": "99152",
      "city": "Belleville",
      "country": "France"
    }
    """, "Address");

// Named entry "AddressList": a list of addresses
List<String> e2 = okyline.validate("""
    [{
      "type": "BILLING",
      "street": "144 avenue des marronniers",
      "zipCode": "99152",
      "city": "Belleville",
      "country": "France"
    }]
    """, "AddressList");

The address payloads above are the schema’s own $defs.Address example values: here they are valid instances of the definition they illustrate.

A library that only publishes definitions writes "$oky": null and is validated through its entries; validating such a contract without an entry name is refused.

Working with several schemas

To handle several contracts in the same application, register each one under an alias you choose - a label independent of the schema’s $id - then validate by alias. It is concurrency-safe and needs no “active schema” to switch.

OkylineApi okyline = new OkylineApi();

// Register each contract under an alias you choose (typically at startup)
okyline.loadSchema("employee", employeeSchema);
okyline.loadSchema("company",  companySchema);

// Validate by alias
List<String> a = okyline.validateWith("employee", employeeJson);
List<String> b = okyline.validateWith("company",  companyJson);

// Named entry on a named schema
List<String> c = okyline.validateWith("company", addressJson, "Address");

The alias is your own label, independent of the schema’s $id. An unknown alias throws UnknownSchemaException (see Error handling).

Resolved validators

resolve(alias) returns an immutable Validator bound to one schema. Hold it, pass it around, call it concurrently - it is unaffected by later re-registration of the same alias.

import io.akwatype.okyline.api.Validator;

Validator company = okyline.resolve("company");

company.validate(companyJson);                 // root
company.validate(addressJson, "Address");      // named entry

Packages

A contract can also be registered from a packaged .okypkg bundle, built by Okyline Studio: loadPackage(alias, bytes) registers its main contract under the alias, together with the dependencies embedded in the package, at the exact versions the package was built with. Afterwards validateWith and resolve address it exactly like a schema registered from text.

byte[] bytes = Files.readAllBytes(Path.of("orders.okypkg"));
okyline.loadPackage("orders", bytes);
okyline.validateWith("orders", orderJson);

A dependency that came along inside the package can be named in turn with bind(alias, schemaId), where schemaId is that schema’s own $id; bind(alias, schemaId, version) picks one version when several coexist.

okyline.bind("address", "common.address");
okyline.validateWith("address", addressJson);

Requiring a final contract

loadSchema, loadPackage and bind accept a TrustLevel: with TrustLevel.FINAL, a contract that does not declare "$state": "FINAL" is refused with a TRUST_LEVEL_NOT_MET error - a policy such as “production runs only what declares itself final”.

okyline.loadSchema("orders", ordersSchema, TrustLevel.FINAL);

Error handling

loadSchema / loadPackage / validate / validateWith return a List<String>:

The library does not throw on invalid data - a validation failure is an expected outcome, returned as the list.

The one exception it raises on purpose is UnknownSchemaException (unchecked), thrown by validateWith(alias, …) and resolve(alias) when no schema is registered under that alias - a programming error, not a data problem.

try {
    okyline.validateWith("wrong-alias", json);
} catch (io.akwatype.okyline.api.UnknownSchemaException e) {
    // register the schema first with loadSchema(alias, schema)
}

Thread-safety

Validation is thread-safe: one OkylineApi instance serves validate(...) / validateWith(...) from many threads concurrently - ideal behind a web server. Loading is not: register schemas and packages while no validation is running, typically at startup or during a pause of the service, never in the middle of validations.

Versions and spec conformance

The library version and the Okyline language specification it implements are decoupled:

OkylineApi.version();      // library version, e.g. "1.9.0"
OkylineApi.specVersion();  // Okyline spec implemented, e.g. "1.8.0"

Library 1.9.0 is stricter at schema load time than the previous library versions: a schema that loaded before may be rejected for a defect that used to pass silently. See the specification changelogs.

Next steps