【最佳实践】JSON校验

Django 发布于 Sep 23, 2021 更新于 Jul 19, 2022

前言

一个段好的代码应该是优雅的,拥有强可维护性、强稳健性、高简洁性,而且还要实现复用。前两点,通过精心的设计和对情况的详尽考虑,是很容易实现的,但是要实现强稳健性,很可能需要牺牲简洁性(下面有个例子讲为什么),于是如何保证简洁性便成为了影响编写优雅代码的关键问题。

我在写后端时经常会有这么一个需求:我定义的某个接口收到了一些请求,这些请求的来源无法保证,内容无法保证,格式无法保证,我要让处理这些请求的流程保持有效,能够对各类异常和非法的请求进行识别和响应。现在,让我展示一下新手(刚开始接触这类开发时的我)会怎么实现这个需求。

一些原始方案

比初学时的我还新的新手

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日

标签