Safe JSON Parse With Result Type
Parse untrusted JSON without exceptions by returning a discriminated Result, with an optional type-predicate validation layer.
Code
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);Report a problem with this exampleOpens a pre-filled issue on GitHub.