fs-safe/docs/json.md
2026-05-06 01:43:39 +01:00

5.1 KiB

JSON files

@openclaw/fs-safe/json is the standalone JSON surface: strict and lenient read variants plus atomic JSON writes.

import {
  tryReadJson,
  readJson,
  readJsonIfExists,
  readJsonSync,
  tryReadJsonSync,
  writeJson,
  writeJsonSync,
  JsonFileReadError,
} from "@openclaw/fs-safe/json";

Three reads, three failure shapes

Same input, three distinct contracts — pick the one whose error story matches your call site:

await readJson<T>("./manifest.json");      // throws JsonFileReadError on missing or invalid
await readJsonIfExists<T>("./cache.json"); // returns null on missing; throws on invalid
await tryReadJson<T>("./optional.json");   // returns null on missing or invalid
Helper Missing file Invalid JSON
readJson throws throws
readJsonIfExists null throws
tryReadJson null null

Use readJson when missing-or-malformed is a programmer error you want to surface immediately. Use readJsonIfExists when "file not there" is normal but malformed bytes should still page someone. Use tryReadJson when neither outcome should crash the caller.

JsonFileReadError carries cause so you can inspect whether the underlying failure was an ENOENT, a SyntaxError, or something else.

Reading

readJson<T>(filePath)

Async strict reader. Throws JsonFileReadError on missing or invalid input. The cast is unchecked — validate the shape with your own schema (zod, valibot, …) if it came from an untrusted source.

const manifest = await readJson<Manifest>("./manifest.json");

readJsonIfExists<T>(filePath)

Async semi-lenient reader. Returns null if the file is missing; throws JsonFileReadError if the file exists but cannot be parsed.

const cache = (await readJsonIfExists<Cache>("./cache.json")) ?? freshCache();

tryReadJson<T>(filePath)

Async lenient reader. Returns null for any failure (missing, unreadable, invalid). The "no fuss" sibling.

const optional = (await tryReadJson<Settings>("./settings.json")) ?? defaults;

readJsonSync<T>(filePath)

Synchronous strict reader. Throws JsonFileReadError on missing or invalid input, matching the async readJson contract.

tryReadJsonSync<T>(pathname)

Synchronous, generic, lenient. Returns T | null. Useful in boot paths where you want a typed result without async.

Writing

writeJson(filePath, value, options?)

Async atomic JSON write. JSON.stringify(value, null, 2) + sibling-temp + rename. Defaults to file mode 0o600.

await writeJson("./state.json", state, { trailingNewline: true });

Options:

type WriteJsonOptions = {
  mode?: number;             // file mode (default 0o600)
  dirMode?: number;          // mode for parent dirs created on demand
  trailingNewline?: boolean; // append "\n" if missing (default false)
};

writeJsonSync(pathname, data)

Synchronous variant. Convenience wrapper that uses the sync atomic-write path with sensible defaults.

writeJsonSync("./prefs.json", { theme: "dark" });

For atomic text writes, use writeTextAtomic from @openclaw/fs-safe/atomic. For in-process serialization, use createAsyncLock from the advanced surface, or prefer jsonStore when you want a JSON-specific read-modify-write helper.

Common patterns

Read-modify-write

const state = (await readJsonIfExists<State>("./state.json")) ?? initialState();
state.lastRun = Date.now();
await writeJson("./state.json", state, { mode: 0o600, dirMode: 0o700 });

Atomic with secure mode

For credentials or other sensitive JSON, write at mode 0o600:

await writeJson("./auth.json", token, { mode: 0o600, dirMode: 0o700 });

For higher-assurance secrets, prefer the dedicated secret-file helpers — they create the parent directory at 0o700 if missing.

Strict load on boot

let manifest: Manifest;
try {
  manifest = await readJson<Manifest>("./manifest.json");
} catch (err) {
  if (err instanceof JsonFileReadError) {
    console.error("manifest unreadable:", err.cause);
    process.exit(1);
  }
  throw err;
}

Concurrent readers, single writer

const state = await readJsonIfExists<State>("./state.json");
// missing returns null; malformed JSON still throws

Error reference

Throw / return When
null (lenient reads) File missing or contents are not valid JSON.
JsonFileReadError readJson or readJsonIfExists saw unreadable or invalid input. Inspect cause.
Native NodeJS.ErrnoException Lower-level fs errors not wrapped.

See also

  • JSON store — a single-file state wrapper with explicit per-call fallback (readOr / updateOr) and optional sidecar locking.
  • Atomic writes — lower-level sibling-temp replacement helpers.
  • Secret files — JSON-or-text writes with mode 0600 in mode 0700 dirs.
  • Private file-store mode — root-bounded JSON+text state stores.
  • File lock — cross-process coordination.