我试图了解是否有一种方法可以验证不同可能类型的 JSON.parse 结果。 在我正在开发的API中,有很多来自数据库的Json字段应该由特定的结构组成,所以我想检查从数据库返回的某个JsonValue是否与给定的类型一致。
我做了一些不同的尝试,这就是我定义通用函数的方式:
parseJsonField<T>(jsonValue: Prisma.JsonValue): T {
return JSON.parse(JSON.stringify(jsonValue));
}
我希望有一些东西可以在 jsonValue 不具有类型 T 的确切属性时抛出错误,但这不起作用。
我正在用这个类进行测试
export class ProgramTagsDTO {
key: string;
value: string;
constructor(key: string, value: string) {
this.key = key;
this.value = value;
}
}
当我运行此测试时:
it('should validate', () => {
const tags: Prisma.JsonValue = [
{
key: 'key1',
value: 'value1',
wrongField: 'value',
},
{
key: 'key2',
value: 'value2',
},
];
let result = null;
const expectedResult = [
new ProgramTagsDTO('key1', 'value1'),
new ProgramTagsDTO('key2', 'value2'),
];
try {
result = programmesService.parseJsonField<ProgramTagsDTO[]>(tags);
} catch (error) {
expect(error).toBeInstanceOf(Error);
}
expect(result).toEqual(expectedResult);
});
我得到:
expect(received).toEqual(expected) // deep equality
- Expected - 2
+ Received + 3
Array [
- ProgramTagsDTO {
+ Object {
"key": "key1",
"value": "value1",
+ "wrongField": "value",
},
- ProgramTagsDTO {
+ Object {
"key": "key2",
"value": "value2",
},
]
但这不是我想要的,我想让该方法抛出异常。 (expect.toEqual只是为了看日志)
经过几天的尝试,我和一位同事想出了这个解决方案,目前看来效果很好:
添加此导入:
import { ClassConstructor, plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
我们为数组和单个值创建了 2 种方法:
public static async parseJsonArray<T>(
cls: ClassConstructor<T>,
json: Prisma.JsonValue,
): Promise<T[]> {
if (!Array.isArray(json)) {
throw new UnprocessableEntityException(
`Json value is not a ${cls.name}[]`,
);
}
const res: Promise<T>[] = [];
for (const element of json) {
res.push(this.parseJsonValue<T>(cls, element));
}
return Promise.all(res);
}
public static async parseJsonValue<T>(
cls: ClassConstructor<T>,
json: Prisma.JsonValue,
): Promise<T> {
const parsed = JSON.parse(JSON.stringify(json));
const transformed: T = plainToClass(cls, parsed, {
excludeExtraneousValues: true,
});
return validate(transformed as object).then((errors) => {
if (errors.length) {
throw new UnprocessableEntityException(
`Json value is not a ${cls.name}, Errors: ${errors}`,
);
}
return transformed;
});
}
plainToClass 处理数组,但我们需要有一些通用的东西,可以根据您正在解析的内容清楚地返回数组或单个对象,因此不可能有像 T | 这样带有返回签名的单个方法。 T[].
不幸的是你不能。与许多语言(例如 Java)一样,编译代码时会删除泛型。在 Typescript 中,情况更糟,“类型”被删除,这意味着它会转换为弱类型的 javascript。
因此,一旦在运行时,以下代码:
parseJsonField<T>(jsonValue: Prisma.JsonValue): T {
return JSON.parse(JSON.stringify(jsonValue));
}
减少为:
parseJsonField(jsonValue) {
return JSON.parse(JSON.stringify(jsonValue));
}
因此,没有类型信息可供检查。
此外,您使用的是一个类,请注意,即使您能够检查返回值是否遵循
T
的接口,它也不会是 T
的 实例,而仅仅是一个泛型对象碰巧具有与
T
实例同名的属性。
您必须在运行时实施检查。与上面发布的解决方案类似,您可以概括它:
// Gets a value and deletes it from the json object
function getDelete<T>(jsonValue :any, property :string, required = false) :T {
const ret = jsonValue[property];
delete jsonValue[property];
if (required && typeof ret === 'undefined') {
throw new Error(`Property ${property} not found`);
}
return ret;
}
// Checks that the json object is empty, otherwise there are extraneous values
function checkEmpty(jsonValue: any) {
if (Object.keys(jsonValue).length > 0) {
throw new Error('Object contains extraneous properties ' + JSON.stringify(jsonValue));
}
}
// Example of a class
public class Person {
private firstName :string;
private lastName :string;
constructor(jsonValue? :any) {
if (jsonValue) {
// This is where we "parse" the json object into the actual class, and check it
this.firstName = getDelete(jsonValue, 'firstName');
this.lastName = getDelete(jsonValue, 'lastName');
}
checkEmpty(jsonValue);
}
}
function parseAndCheck<T>(jsonValue :any, constructor: (...arg: any[])=>T) :T {
return constructor(JSON.parse(JSON.stringify(jsonValue)));
}
(尚未测试上面的代码,只是为了给您一个想法)