【最佳实践】JSON校验
前言
一个段好的代码应该是优雅的,拥有强可维护性、强稳健性、高简洁性,而且还要实现复用。前两点,通过精心的设计和对情况的详尽考虑,是很容易实现的,但是要实现强稳健性,很可能需要牺牲简洁性(下面有个例子讲为什么),于是如何保证简洁性便成为了影响编写优雅代码的关键问题。
我在写后端时经常会有这么一个需求:我定义的某个接口收到了一些请求,这些请求的来源无法保证,内容无法保证,格式无法保证,我要让处理这些请求的流程保持有效,能够对各类异常和非法的请求进行识别和响应。现在,让我展示一下新手(刚开始接触这类开发时的我)会怎么实现这个需求。
一些原始方案
比初学时的我还新的新手
def request_handler(req: HttpRequest) -> JsonResponse:
try:
# do some preprocessing...
except Exception:
return JsonResponse({
'code': 1,
'msg': "internal error"
})
# do something...
return JsonResponse({
'code': 0,
'msg': "success"
})
看起来解决了问题,实际上产生了很多别的问题。首先,try-catch会隐藏逻辑错误,你只知道它出问题了,但是不知道出了什么问题。也许你会在日志里打印异常信息,但是不可否认的是很多异常信息是低可读性的,你并不能通过阅读日志立刻了解是哪里出了问题。这样的处理方式有可能会让程序产生一些不可控因素(鬼知道非法输入在try-catch里面干了什么事情)
初学时的我
这是我做大创项目时的一段代码
def queryCardPage(request: HttpRequest) -> JsonResponse:
# 预先定义基础的返回 Json
respj = {
'code': None,
'message': None,
'data': {}
}
data = respj['data']
# 检查请求类型
if request.method != 'GET':
respj['code'] = 0
respj['message'] = "bad request"
return JsonResponse(respj)
courtId = request.GET.get('court')
pageSize = request.GET.get('size')
pageAt = request.GET.get('page')
cardType = request.GET.get('type')
targetType = request.GET.get('target')
searchText = request.GET.get('name')
normal = request.GET.get('normal')
student = request.GET.get('student')
faculty = request.GET.get('faculty')
# 参数合法性校验
if courtId is None or pageSize is None or pageAt is None \
or not v.integerValidate(courtId) \
or not v.integerValidate(pageSize, positive=True) \
or not v.integerValidate(pageAt, positive=True) \
or (normal is not None and not v.integerValidate(normal)) \
or (student is not None and not v.integerValidate(student)) \
or (faculty is not None and not v.integerValidate(faculty)) \
or (cardType is not None and not v.integerValidate(cardType, positive=True)) \
or (targetType is not None and not v.integerValidate(targetType, positive=True)):
respj['code'] = 0
respj['message'] = "bad request"
return JsonResponse(respj)
# do something HERE...
respj['code'] = 0
respj['message'] = "success"
respj['data'] = '...'
return JsonResponse(respj)
好的!我成功完成了这个需求,为do something HERE...
保证了输入的合法性!就是…这段处理又臭又长🤮
对,这个原始的方案非常的麻烦、繁琐,可维护性和可读性也不高,下面看看有什么更优雅的方案
更优雅的解决方案
2021年9月19日,我又开始写后端,这次因为有了一些经验,便开始思考有没有更合理的方式处理这个问题
一些思考
能不能用一个函数去完成验证?稍微思考了一下,一个函数想给所有接口用,这不是扯淡吗?每个接口要求的字段都不一样,还有对字段内容更细粒度的要求,pass
运用一下自动机的思维?编写一个人类可读的语法定义式,为每个接口单独写一个对输入的语法定义,然后去构建这个语法定义的自动机,在请求发过来时,用自动机去匹配这个请求?感觉可行,拥有通用的合法性判断能力。来找找有没有轮子可以用!
JSON Schema
JSON Schema是一个开放的标准格式,它用于描述一个“合法的JSON文档”的格式,其中一个重要用途就是实例验证,下面来看一个例子:
{
"type": "object",
"required": ["title"],
"properties": {
"title": {"type": "string"},
"tag": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"author": {"type": "string", "minLength": 1},
"gallery": {"type": "string", "minLength": 1},
"minRating": {"type": "number", "minimum": 0, "maximum": 10}
}
}
这个JSON Schema文档(下面称Pattern)定义了一个合法输入的格式,要求输入的JSON文档(下面称这个文档为A)满足它定义的要求:A中一定要出现”title”字段,其余字段可选,并且,A中只要出现了在Pattern的properties中定义的字段,该字段就必须满足properties中对它的限制条件。
光有这个Pattern还做不了对JSON的验证,我们还需要拥有对JSON Schema验证器的实现(轮子)
下面的连接指向一个开源项目库,里面列举了用不同语言实现JSON Schema校验的开源项目
JSON Schema is a vocabulary that allows you to annotate and validate JSON documents.
JSON Schema | The home of JSON Schema (json-schema.org)
这里我们以Python的 jsonschema 为例,这个第三方开源包实现了一个JSON Schema validator,直接用Python的字典表示JSON Schema即可
import json
import jsonschema
schema = {
'type': 'object',
'required': ['...'],
'properties': {
'...': {'type': '...'}
}
}
def request_handler(http_req: HttpRequest) -> JsonResponse:
if http_req.method == 'GET':
return JsonResponse({
'code': 1,
'msg': "not post"
})
try:
req = json.loads(http_req.body)
jsonschema.validate(req, schema=schema)
except JSONDecodeError:
return JsonResponse({
'code': 1,
'msg': 'not json format'
})
except ValidationError as e:
return JsonResponse({
'code': 1,
'msg': f"bad request format: {e.message}"
})
# do something HERE...
return JsonResponse({
'code': 0,
'msg': "success",
'response': '...'
})
运行到do something HERE...
的时候,就能确保req是一个合法的JSON(字典)请求了
这里还可以进行进一步抽象,可以把do something HERE...
抽象出来,放到service层内,于是这里的request_handler变成为了纯粹的controller组件
👆通过这种方式,可以实现业务层与控制层的完全分离,逻辑上也会清晰很多,分离后业务层的代码也可以被其他控制层函数复用,可以消灭一大堆冗余代码
结语
最佳实践需要有一定的开发经验才可以理解,这篇文章中也体现了我一些曲曲折折的尝试路径,希望本篇笔记可以帮助一些人少走一点点弯路~
2021年9月22日