一个 json 转换工具
在前后端的数据协议(主要指http
和websocket
)的问题上,如果前期沟通好了,那么数据协议上问题会很好解决,前后端商议一种都可以接受的格式即可。但是如果接入的是老系统、第三方系统,或者由于某些奇怪的需求(如为了节省流量,json 数据使用单字母作为key
值,或者对某一段数据进行了加密),这些情况下就无法商议,需要在前端做数据转换,如果不转换,那么奔放的数据格式可读性差,也会造成项目难以维护。
这也正是我在项目种遇到的问题,网上也找了一些方案,要么过于复杂,要么有些功能不能很好的支持,于是有了这个工具 class-converter。欢迎提 issue 和 star~~https://github.com/zquancai/class-converter
下面我们用例子来说明下:
面对如下的Server
返回的一个用户user
数据:
1 2 3 4 5 | { "i" : 1234, "n" : "name" , "a" : "1a2b3c4d5e6f7a8b" } |
或者这个样的:
1 2 3 4 5 | { "user_id" : 1234, "user_name" : "name" , "u_avatar" : "1a2b3c4d5e6f7a8b" } |
数据里的 avatar
字段在使用时,可能需要拼接成一个 url
,例如 https://xxx.cdn.com/1a2b3c4d5e6f7a8b.png
。
当然可以直接这么做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | const json = { "i" : 1234, "n" : "name" , "a" : "1a2b3c4d5e6f7a8b" , }; const data = {}; const keyMap = { i: 'id' , n: 'name' , a: 'avatar' , } Object.entries(json).forEach(([key, value]) => { data[keyMap[key]] = value; }); // data = { id: 1234, name: 'name', avatar: '1a2b3c4d5e6f7a8b' } |
然后我们进一步就可以把这个抽象成一个方法,像下面这个样:
1 2 3 4 5 6 7 | const jsonConverter = (json, keyMap) => { const data = {}; Object.entries(json).forEach(([key, value]) => { data[keyMap[key]] = value; }); return data; } |
如果这个数据扩展了,添加了教育信息,user
数据结构看起来这个样:
1 2 3 4 5 6 7 8 9 | { "i" : 1234, "n" : "name" , "a" : "1a2b3c4d5e6f7a8b" , "edu" : { "u" : "South China Normal University" , "ea" : 1 } } |
此时的 jsonConverter
方法已经无法正确转换 edu
字段的数据,需要做一些修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | const json = { "i" : 1234, "n" : "name" , "a" : "1a2b3c4d5e6f7a8b" , "edu" : { "u" : "South China Normal University" , "ea" : 1 } }; const data = {}; const keyMap = { i: 'id' , n: 'name' , a: 'avatar' , edu: { key: 'education' , keyMap: { u: 'universityName' , ea: 'attainment' } }, } |
随着数据复杂度的上升,keyMap 数据结构会变成一个臃肿的配置文件,此外 jsonConverter
方法会越来越复杂,以至于后面同样难以维护。但是转换后的数据格式,对于项目来说,数据的可读性是很高的。所以,这个转换必须做,但是方式可以更优雅一点。
写这个工具的初衷也是为了更优雅的进行数据转换。
工具用法
还是上面的例子(这里使用typescript
写法):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import { toClass, property } from 'class-converter' ; // 待解析的数据 const json = { "i" : 1234, "n" : "name" , "a" : "1a2b3c4d5e6f7a8b" , }; class User { @property( 'i' ) id: number; @property( 'n' ) name: string; @property( 'a' ) avatar: string; } const userIns = toClass(json, User); |
你可以轻而易举的获得下面的数据:
1 2 3 4 5 6 7 | // userIns 是 User 的一个实例 const userIns = { id: 1234, name: 'name' , avatar: '1a2b3c4d5e6f7a8b' , } userIns instanceof User // true |
Json
类既是文档又是类似于上文说的与keyMap
类似的配置文件,并且可以反向使用。
1 2 3 4 5 6 7 8 | import { toPlain } from 'class-converter' ; const user = toPlain(userIns, User); // user 数据结构 { i: 1234, n: 'name' , a: '1a2b3c4d5e6f7a8b' , }; |
这是一个最简单的例子,我们来一个复杂的数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | { "i" : 10000, "n" : "name" , "user" : { "i" : 20000, "n" : "name1" , "email" : "zqczqc" , // {"i":1111,"n":"department"} "d" : "eyJpIjoxMTExLCJuIjoiZGVwYXJ0bWVudCJ9" , "edu" : [ { "i" : 1111, "sn" : "szzx" }, { "i" : 2222, "sn" : "scnu" }, { "i" : 3333 } ] } } |
这是后端返回的一个叫package
的json对象,字段意义在文档中这么解释:
- i:package 的 id
- n:package 的名字
- user:package 的所有者,一个用户
- i:用户 id
- n:用户名称
- email:用户email,但是只有邮箱前缀
- d:用户的所在部门,使用了base64编码了一个json字符串
- i:部门 id
- n:部门名称
- edu:用户的教育信息,数组格式
- i:学校 id
- sn:学校名称
我们的期望是将这一段数据解析成,不看文档也能读懂的一个json
对象,首先我们经过分析得出上面一共有4类实体对象:package、用户信息、部门信息、教育信息。
下面是代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | import { toClass, property, array, defaultVal, beforeDeserialize, deserialize, optional } from 'class-converter' ; // 教育信息 class Education { @property( 'i' ) id: number; // 提供一个默认值 @defaultVal( 'unknow' ) @prperty( 'sn' ) schoolName: string; } // 部门信息 class Department { @property( 'i' ) id: number; @prperty( 'n' ) name: string; } // 用户信息 class User { @property( 'i' ) id: number; @property( 'n' ) name: string; // 保留一份邮箱前缀数据 @optional() @property() emailPrefix: string; @optional() // 这里希望自动把后缀加上去 @deserialize(val => `${val}@xxx.com`) @property() email: string; @beforeDeserialize(val => JSON.parse(atob(val))) @typed(Department) @property( 'd' ) department: Department; @array() @typed(Education) @property( 'edu' ) educations: Education[]; } // package class Package { @property( 'i' ) id: number; @property( 'n' ) name: string; @property( 'user' , User) owner: User; } |
数据已经定义完毕,这时只要我们执行toClass
方法就可以得到我们想要的数据格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | { id: 10000, name: 'name' , owner: { id: 20000, name: 'name1' , emailPrefix: 'zqczqc' , email: "zqczqc@xxx.com" , department: { id: 1111, name: 'department' }, educations: [ { id: 1111, schoolName: 'szzx' }, { id: 2222, schoolName: 'scnu' }, { id: 3333, schoolName: 'unknow' } ] } } |
上面这一份数据,相比后端返回的数据格式,可读性大大提升。这里的用法出现了@deserialize
、@beforeDeserialize
、@yped
的装饰器,这里对这几个装饰器是管道方式调用的(前一个的输出一个的输入),这里做一个解释:
beforeDeserialize
第一个参数可以最早拿到当前属性值,这里可以做一些解码操作typed
这个是转换的类型,入参是一个类,相当于自动调用toClass
,并且调动时的第一个参数是beforeDeserialize
的返回值或者当前属性值(如果没有@beforeDeserialize
装饰器)。如果使用了@array
装饰器,则会对每一项数组元素都执行这个转换deserialize
这个装饰器是最后执行的,第一个参数是beforeDeserialize
返回值,@typed
返回值,或者当前属性值(如果前面两个装饰器都没设置的话)。在这个装饰器里可以做一些数据订正的操作
这三个装饰器是在执行toClass
时才会调用的,同样的,当调用toPlain
时也会有对应的装饰器@serialize
、@fterSerialize
,结合@typed
进行一个相反的过程。下面将这两个转换过程的流程绘制出来。
调用 toClass
的过程:
调用 toPlain
的过程是调用 toClass
的逆过程,但是有些许不一样,有一个注意点就是:在调用 toClass
时允许出现一对多的情况,就是一个属性可以派生出多个属性,所以调用调用 toPlain
时需要使用 @serializeTarget
来标记使用哪一个值作为逆过程的原始值,具体用法可以参考文档。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?