Serialisation
Quando definitions are JSON-compatible documents. You can store them with
JSON.stringify, read them with JSON.parse, and pass the result to a Quando
parser.
The parsers accept unknown. They validate the complete document and restore
the methods supplied by the builders.
Rule builders add .and, .or, and .except as non-enumerable methods.
JSON storage sees the rule fields without seeing those functions.
import { dates, parseRule, timeOfDayRange, weekdays } from "@kensio/quando";
const openingHours = weekdays() .and(timeOfDayRange("09:00", "17:00")) .except(dates("2026-12-25"));
const stored = JSON.stringify(openingHours);const restored = parseRule(JSON.parse(stored));
const withLunchBreak = restored.except(timeOfDayRange("12:30", "13:30"));parseRule reports the path to an invalid field:
parseRule({ type: "all", rules: [ { type: "daysOfWeek", days: ["mondey"] }, { type: "timeOfDay", from: "09:00", to: "17:00" }, ],});TypeError: rule.rules[0].days[0]: "mondey" is not a day of the week. Expected one of monday, tuesday, wednesday, thursday, friday, saturday, sundayA custom rule stores its name
Section titled “A custom rule stores its name”A custom rule document stores its type name and JSON-compatible options.
Supply the implementation through context.rules when evaluating the rule:
import { customRule, parseRule } from "@kensio/quando";
const stored = JSON.stringify(customRule("easter", { offset: 1 }));const restored = parseRule(JSON.parse(stored));
console.log(stored);{"type":"custom","name":"easter","options":{"offset":1}}Parsing validates the document without requiring the custom rule’s
implementation. You can store or forward a schedule before loading its
registry. Evaluating a custom rule missing from the registry throws
UnknownCustomRuleError.
Options must survive a JSON round trip. parseRule rejects unsupported
values. See rules for custom rule
implementations.
Schedules
Section titled “Schedules”import { parseSchedule, schedule, weekdays } from "@kensio/quando";
const office = schedule({ zone: "Europe/London" }).open( weekdays(), "09:00-17:00", { label: "Regular office hours", comment: "Customers can book appointments during these hours.", },);
const stored = JSON.stringify(office);const restored = parseSchedule(JSON.parse(stored));The parsed schedule retains its zone, methods, labels, and comments. Explanations continue to include the stored labels and comments.
Rota values belong to your application. Pass a value parser to parseRota so
Quando can validate them.
import { parseString, parseRota, rota, weekdays } from "@kensio/quando";
const onCall = rota().assign(weekdays(), "alice");const stored = JSON.stringify(onCall);const restored = parseRota(JSON.parse(stored), parseString);parseString and parseBoolean cover those primitive types. A custom parser
receives the value and its path:
import { rota, weekdays } from "@kensio/quando";import { fail, parseRota, type ValueParser } from "@kensio/quando/parsing";
interface Engineer { readonly id: string;}
const asEngineer: ValueParser<Engineer> = (value, path) => { if ( typeof value !== "object" || value === null || !("id" in value) || typeof value.id !== "string" ) { return fail(path, "expected an engineer with a string id."); } return { id: value.id };};
const storedRota = JSON.stringify( rota<Engineer>().assign(weekdays(), { id: "alice" }),);const restored = parseRota(JSON.parse(storedRota), asEngineer);Tallies
Section titled “Tallies”import { parseTally, tally, weekdays } from "@kensio/quando";
const staff = tally().plus(weekdays(), 3);const restored = parseTally(JSON.parse(JSON.stringify(staff)));parseTally requires finite numbers and a cascade that uses the sum
strategy.
Cascades
Section titled “Cascades”Use parseCascade for a low-level cascade. Supply a parser for its values.
import { parseString, parseCascade } from "@kensio/quando/parsing";
const stored: unknown = { type: "cascade", layers: [ { scope: { type: "daysOfWeek", days: ["monday"] }, value: "alice", }, ],};
const onCall = parseCascade(stored, parseString);The parser checks nested replacement cascades and validates values against the cascade’s merge strategy.
Values that can be stored
Section titled “Values that can be stored”JsonValue is the union of supported JSON value types. JsonCompatible<T>
checks whether your application type can be stored, including object types
without an index signature.
Valid values include strings, finite numbers, booleans, null, arrays, and
plain objects. Constructors reject values that JSON would lose or change:
| Value | Reason |
|---|---|
undefined, functions, and symbols |
JSON drops them |
bigint |
JSON cannot encode it |
NaN and infinity |
JSON changes them to null |
| Class instances | JSON loses their prototype |
| Circular objects | JSON cannot traverse them |
| Symbol-keyed properties | JSON omits them |
| Non-enumerable properties | JSON omits them |
Changing a stored format
Section titled “Changing a stored format”Parsers reject unknown fields. Deploy code that can read a new field before deploying code that writes it.
Parsers throw ParseError, a subclass of TypeError with path and code
fields. The code is "invalid-value" or "unknown-field".
All parsers take decoded data. Use parseSchedule(JSON.parse(text)) when the
input is JSON text.
After parsing a custom rule document, reattach its executable registry with
restored.withCustomRules(registry) before querying it.