news 2026/9/23 23:45:06

fp-ts Json 模块完全指南:基于 Either 的安全 JSON 解析与序列化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
fp-ts Json 模块完全指南:基于 Either 的安全 JSON 解析与序列化

fp-ts Json 模块完全指南:基于 Either 的安全 JSON 解析与序列化

【免费下载链接】fp-tsFunctional programming in TypeScript项目地址: https://gitcode.com/gh_mirrors/fp/fp-ts

导读

fp-ts/Json模块(自 v2.10.0 起提供)为 TypeScript 函数式编程提供了不抛异常的 JSON 解析与序列化方案:它把 JavaScript 内置的JSON.parse/JSON.stringify包装成返回Either<unknown, Json>的纯函数,让错误成为可组合、可追踪的一等公民。读完本文,你将掌握JsonJsonArrayJsonRecord三个核心类型的准确含义,学会用parse安全解析字符串、用stringify安全序列化任意值,并能通过pipeEither的各种组合子把它们无缝接入 fp-ts 的任意函数式管线中。


一、模块概览:为什么 JSON 操作需要"函数式"封装

JavaScript 原生的JSON.parseJSON.stringify可能抛异常的函数:解析一段非法 JSON 会抛出SyntaxError,序列化含循环引用的对象会抛出TypeError。在 fp-ts 的编程范式里,异常流无法被pipemapchain等组合子感知,会破坏类型的纯净性。

Json模块给出的答案是:用 tryCatch(位于src/Either.ts)把这两个 API 包装成返回Either<unknown, Json>的纯函数。tryCatch的核心实现非常直白(见 src/Either.ts#L1393-L1399):

export const tryCatch = <E, A>(f: LazyArg<A>, onThrow: (e: unknown) => E): Either<E, A> => { try { return right(f()) } catch (e) { return left(onThrow(e)) } }

即:正常返回right(结果),任何异常都经onThrow转换后放入leftJson模块传给onThrow的是identity(见 src/Json.ts),因此错误类型就是unknown,原始异常对象(如SyntaxErrorTypeError)会被原样保留在Either的左侧,供后续mapLeft精确检查。

从模块导出看,src/index.ts在第 58 行import * as json from './Json'、第 385 行将整个模块作为json命名空间导出,因此你可以用import * as J from 'fp-ts/Json'import { json } from 'fp-ts'两种方式引入。


二、类型体系:Json / JsonArray / JsonRecord

Json模块定义了三个互相递归的类型,完整刻画了"合法 JSON 值"的形态(见 src/Json.ts#L10-L22):

export type Json = boolean | number | string | null | JsonArray | JsonRecord export interface JsonRecord { readonly [key: string]: Json } export interface JsonArray extends ReadonlyArray<Json> {}
类型定义说明
Jsonboolean \| number \| string \| null \| JsonArray \| JsonRecordJSON 值的联合类型,即"值"层面的完整定义
JsonRecordreadonly [key: string]: Json键为字符串、值为Json的只读记录,即 JSON 对象
JsonArrayextends ReadonlyArray<Json>Json元素的只读数组,即 JSON 数组

几个值得注意的设计细节:

  • 没有undefined、函数、Symbolbigint:这些都不是合法 JSON 值,因而被类型系统直接排除。dtslint/Json.ts中用@ts-expect-error断言了undefined、箭头函数、Symbol()、含undefined字段的对象等都无法通过stringify<Json>的类型检查(见 dtslint/Json.ts#L9-L20)。
  • 对象与数组是"只读"(readonly)形态JsonRecord的索引签名和JsonArray的数组类型都带readonly,与 fp-ts 一贯倡导的不可变数据结构风格一致,也方便与ReadonlyArrayReadonlyRecord等模块对接。
  • 递归结构Json引用JsonArrayJsonRecord,而后两者又引用Json,从而支持任意深度的嵌套 JSON(对象套数组、数组套对象……)。

三、parse:安全解析 JSON 字符串

parse将一段 JSON 字符串转换为Json类型(对应JSON.parse),其签名与实现如下:

export declare const parse: (s: string) => Either<unknown, Json> // 实现(src/Json.ts#L37) export const parse = (s: string): Either<unknown, Json> => tryCatch(() => JSON.parse(s), identity)

行为语义

  • 输入合法 JSON→ 返回E.right(json),其中json已被JSON.parse反序列化为实际 JavaScript 值;
  • 输入非法 JSON→ 底层JSON.parse抛出SyntaxError,被tryCatch捕获后包装为E.left(syntaxError),异常对象本身原样保留。

官方示例(可直接运行)

import * as J from 'fp-ts/Json' import * as E from 'fp-ts/Either' import { pipe } from 'fp-ts/function' assert.deepStrictEqual(pipe('{"a":1}', J.parse), E.right({ a: 1 })) assert.deepStrictEqual( pipe('{"a":}', J.parse), E.left(new SyntaxError(`Unexpected token '}', "{"a":}" is not valid JSON`)) )

上面第二个断言展示了一个关键事实:解析失败的left分支携带的是真实的SyntaxError实例,其message与原生JSON.parse抛出的完全一致,便于日志记录或mapLeft分类处理。

测试验证

在 test/Json.ts#L7-L13 中,这两个场景均被测试用例覆盖:

it('parse', () => { U.deepStrictEqual(pipe('{"a":1}', _.parse), E.right({ a: 1 })) U.deepStrictEqual( pipe('{"a":}', _.parse), E.left(new SyntaxError(`Unexpected token '}', "{"a":}" is not valid JSON`)) ) })

四、stringify:安全序列化任意 JavaScript 值

stringify将任意 JavaScript 值转换为 JSON 字符串(对应JSON.stringify),签名与实现如下:

export declare const stringify: <A>(a: A) => Either<unknown, string> // 实现(src/Json.ts#L60-L67) export const stringify = <A>(a: A): Either<unknown, string> => tryCatch(() => { const s = JSON.stringify(a) if (typeof s !== 'string') { throw new Error('Converting unsupported structure to JSON') } return s }, identity)

行为语义

与裸用JSON.stringify相比,stringify有两个层次的保护:

  1. 异常捕获JSON.stringify遇到循环引用时会抛出TypeError: Converting circular structure to JSON,该异常被tryCatch捕获并放入left
  2. 返回类型守卫JSON.stringify在遇到undefined、函数或Symbol作为顶层输入时,会返回undefined而非字符串。实现里显式检查typeof s !== 'string',一旦出现这种情况就抛出new Error('Converting unsupported structure to JSON'),从而保证right分支永远是真正的字符串,杜绝"看似成功实则拿到 undefined"的隐性 bug。

官方示例(可直接运行)

import * as E from 'fp-ts/Either' import * as J from 'fp-ts/Json' import { pipe } from 'fp-ts/function' assert.deepStrictEqual(J.stringify({ a: 1 }), E.right('{"a":1}')) const circular: any = { ref: null } circular.ref = circular assert.deepStrictEqual( pipe( J.stringify(circular), E.mapLeft((e) => e instanceof Error && e.message.includes('Converting circular structure to JSON')) ), E.left(true) )

第二个断言展示了stringifyE.mapLeft的经典配合:序列化失败不再是静默的运行时异常,而是可以被函数式地检查、映射与分支处理

测试验证

test/Json.ts#L15-L35 覆盖了四种典型输入:

输入期望结果
{ a: 1 }E.right('{"a":1}')
含循环引用的对象E.left(...),且message'Converting circular structure to JSON'
类型化对象{ name: 'Giulio', age: 45 }E.right('{"name":"Giulio","age":45}')
undefined(顶层)E.left(new Error('Converting unsupported structure to JSON'))

其中最后一个用例直接验证了"返回类型守卫"分支:_.stringify(undefined as any)必须返回E.left(new Error('Converting unsupported structure to JSON'))

类型层面的约束

虽然stringify的类型签名是<A>(a: A) => Either<unknown, string>(接受任意类型),但配合Json类型使用时可获得更强的编译期保障。dtslint/Json.ts展示了类型测试的意图:

  • 以下调用必须报类型错误(@ts-expect-error):stringify<Json>(undefined)stringify<Json>(() => {})stringify<Json>(Symbol())stringify<Json>({ a: undefined })(见 dtslint/Json.ts#L9-L20);
  • 而合法的 JSON 形状(如{ a: 'a', b: 1 }及其数组、展开副本)可以顺利通过(见 dtslint/Json.ts#L22-L37)。

五、组合使用:把 parse / stringify 接入函数式管线

由于parsestringify都返回Either,它们可以无缝接入 fp-ts 的Either组合子体系。官方示例和 dtslint 用例给出了典型用法:

1. 链式校验与转换

import * as E from 'fp-ts/Either' import { pipe } from 'fp-ts/function' // 解析 → 校验 → 转换,全程类型安全 const result = pipe( '{"a":1}', J.parse, E.chain((json) => /* 在这里对 json 做进一步校验/转换,返回 Either<unknown, X> */ E.right(json)), E.map((x) => /* 转换成功后的值 */ x) )

2. 用 chainFirst 挂接副作用

dtslint 中的用例pipe(E.right('a'), E.chainFirst(_.stringify))(见 dtslint/Json.ts#L39-L40)表明stringify可直接用于chainFirst:在保持左侧值不变的同时,把序列化结果作为校验步骤——若序列化失败,整个管道立即短路为left

// $ExpectType Either<unknown, string> pipe(E.right('a'), E.chainFirst(_.stringify))

3. 错误分类处理

const classify = (e: unknown): string => e instanceof SyntaxError ? `JSON 语法错误: ${e.message}` : e instanceof Error ? `其他错误: ${e.message}` : `未知错误: ${String(e)}` const safeParse = (s: string) => pipe(s, J.parse, E.mapLeft(classify))

六、与其他模块的关系及版本背景

  • 依赖Json模块仅依赖EithertryCatch)与functionidentity),依赖面极小,可放心引入(见 src/Json.ts#L4-L5)。
  • 版本引入:该模块于 v2.10.0 加入(CHANGELOG 中 "addJsonmodule" 条目)。CHANGELOG 同时记录了后续版本中,原先分散在其他模块的Json类型、parseJSONstringifyJSON等旧 API 被标记废弃,官方明确建议"使用Json模块替代"(见 CHANGELOG.md)。因此新代码应统一从fp-ts/Json导入。

七、小结与使用建议

fp-ts/Json模块的价值在于:把最容易"悄悄抛异常"的两个 JSON 操作,变成了显式、可组合、类型可追踪的Either计算。核心要点回顾:

  • Json/JsonArray/JsonRecord三个递归类型精确刻画了合法 JSON 值,天然排除undefined、函数等非法输入;
  • parse返回Either<unknown, Json>,解析失败时左侧携带原始SyntaxError
  • stringify返回Either<unknown, string>,既捕获循环引用异常,又用显式检查杜绝"顶层值为 undefined 却返回非字符串"的隐患;
  • 二者均可通过pipechainchainFirstmapLeft等组合子接入任意 Either 管线,错误处理从"try/catch 包围"升级为"数据流中的分支"。

推荐的使用姿势:在解析外部输入(HTTP 响应体、配置文件、本地存储读取)时,一律用J.parse+E.mapLeft显式分类错误;在序列化可能含循环引用或顶层非字符串值的数据时,使用J.stringify并在left分支统一记录日志。这样既保留了原生JSONAPI 的性能与语义,又让整个错误路径始终处于类型系统的掌控之中。

【免费下载链接】fp-tsFunctional programming in TypeScript项目地址: https://gitcode.com/gh_mirrors/fp/fp-ts

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 23:40:54

时区转换避坑指南:从UTC到中国标准时间的正确姿势

之前接了一个报表需求&#xff0c;上游给的数据里时间字段是UTC存储的日期字符串&#xff0c;要求落库时转成中国标准时间并输出“XXXX-XX-XX”这种格式。第一版写得很顺&#xff1a;解析字符串、转时区、格式化、返回&#xff0c;一气呵成。结果上线第一天就被人反馈“日期对不…

作者头像 李华
网站建设 2026/9/23 23:37:52

FastDFS多存储目录配置详解:原理、实操与排错指南

做运维的兄弟应该都有过这种经历&#xff1a;项目跑着跑着磁盘满了&#xff0c;登录上去一看&#xff0c;FastDFS 的存储目录写得满满当当&#xff0c;日志和文件挤在一起&#xff0c;想扩容又不敢乱动。一开始我也以为 FastDFS 写多个目录就是改几个配置项的事&#xff0c;直到…

作者头像 李华
网站建设 2026/9/23 23:37:37

JavaWeb仿小米商城源码:Servlet+JSP+MySQL完整项目

简介&#xff1a;本资源是一套高质量JavaWeb课程设计级仿小米在线商城实战项目&#xff0c;面向高校计算机专业学生及JavaWeb初学者&#xff0c;解决Web开发综合实践能力训练与大作业交付需求。项目完整实现用户注册登录、商品浏览、购物车管理、订单提交等核心电商功能&#x…

作者头像 李华
网站建设 2026/9/23 23:36:53

图书馆座位预约管理系统:从抢座乱象到扫码落座的完整落地路径

简介&#xff1a;这份资源是《图书馆座位预约管理系统》的完整Java项目源码包&#xff0c;面向学习Java Web开发的学生与初级开发者&#xff0c;用于掌握从需求分析到系统落地的全过程。系统围绕座位状态查看、预约、取消及超时自动释放等核心功能展开&#xff0c;采用表现层、…

作者头像 李华
网站建设 2026/9/23 23:34:45

Jev-TypeSafe-ai系统一模型构建Skill

名称TypeSafe 系统一模型构建开源协议MIT描述> 使用 TypeSafe 构建 AI 驱动的软件&#xff1a;小单元的人工智能能力&#xff0c; 可以像编程原语一样使用。它的 System One 模型&#xff0c;包括 Jev&#xff0c; 将自然语言和应用状态转化为代码可以组合的类型化判断和概率…

作者头像 李华
网站建设 2026/9/23 23:34:13

BRATS 2021脑肿瘤分割数据集完整实践:从申请到训练

最近在研究脑肿瘤分割方向&#xff0c;绕不开的一个东西就是BRATS 2021数据集。这是脑肿瘤分割领域公认的标准benchmark&#xff0c;几乎所有顶会论文的对比实验里都会出现它的身影。我大概花了两周时间&#xff0c;把这个数据集从申请、下载、预处理到跑通一套3D分割模型的完整…

作者头像 李华