Safe JSON Parse With Result Type

Parse untrusted JSON without exceptions by returning a discriminated Result, with an optional type-predicate validation layer.

TypescriptData Integrity

Code

snippet
export type Result<Value, Failure> =
  | { readonly ok: true; readonly value: Value }
  | { readonly ok: false; readonly error: Failure };

export type JsonParseError =
  | { readonly reason: "syntax"; readonly message: string }
  | { readonly reason: "validation"; readonly message: string };

export function parseJson<Value = unknown>(text: string): Result<Value, JsonParseError> {
  if (typeof text !== "string") throw new TypeError("text must be a string");
  let parsed: unknown;
  try {
    parsed = JSON.parse(text);
  } catch (error) {
    return {
      ok: false,
      error: { reason: "syntax", message: error instanceof Error ? error.message : String(error) },
    };
  }
  return { ok: true, value: parsed as Value };
}

export function parseJsonWith<Value>(
  text: string,
  guard: (value: unknown) => value is Value,
): Result<Value, JsonParseError> {
  if (typeof guard !== "function") throw new TypeError("guard must be a type predicate function");
  const result = parseJson<unknown>(text);
  if (!result.ok) return result;
  if (!guard(result.value)) {
    return {
      ok: false,
      error: { reason: "validation", message: "value did not match the expected shape" },
    };
  }
  return { ok: true, value: result.value };
}

interface User {
  readonly id: string;
  readonly name: string;
}

function isUser(value: unknown): value is User {
  if (value === null || typeof value !== "object") return false;
  const record = value as Record<string, unknown>;
  return typeof record.id === "string" && typeof record.name === "string";
}

const valid = parseJsonWith('{"id":"u1","name":"Ada"}', isUser);
if (valid.ok) console.log(valid.value.name);
else console.log(valid.error.reason);

const invalid = parseJsonWith("{oops}", isUser);
console.log(invalid);

const wrongShape = parseJsonWith('{"id":1}', isUser);
console.log(wrongShape);