← 中文资源

简体中文版本

返回 Result 的安全 JSON 解析

用可辨识联合 Result 代替异常解析不可信 JSON,并可叠加类型谓词校验。

阅读英文原文

功能说明

该示例把 JSON.parse 改造成返回 Result 的安全接口。解析成功时返回 ok 为 true 与值,失败时返回 ok 为 false 与结构化错误,调用方无需 try/catch 也不会漏掉异常。parseJsonWith 进一步接收类型谓词,在语法正确但结构不符合预期时返回 validation 错误,适合处理来自网络、文件或消息队列的不可信数据。

关键代码

Result 是可辨识联合,成功分支带 value,失败分支带 error,TypeScript 能根据 ok 自动收窄。parseJson 捕获 JSON.parse 的异常并把原因归为 syntax。parseJsonWith 先调用 parseJson 得到 unknown,再用传入的类型谓词检查;谓词不通过时返回 validation,通过后把 unknown 收窄为泛型 Value。示例 isUser 检查对象非空且 id、name 都是字符串。

使用注意

parseJson 的泛型断言不会做任何运行时校验,只有 parseJsonWith 才安全。类型谓词必须真正检查所有关键字段,不能只判断一个字段就返回 true。JSON.parse 不接受注释、尾逗号与 BigInt,超大文本也会占用大量内存,必要时先限制长度。解析出的对象仍可能包含 proto 等键,后续合并时要使用安全策略。

验证方式

断言合法用户 JSON 返回 ok 且能读取 name,语法错误返回 reason 为 syntax,字段类型错误返回 reason 为 validation。测试顶层数组、null 与缺少字段的对象都被谓词拒绝。传入非函数谓词断言 TypeError,并确认所有失败分支都没有抛出异常。

代码示例

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);