利用 JSON-Schema 对 Json 数据进行校验( Python 示例)

转载:https://cloud.tencent.com/developer/article/1005810

梅海峰发表于梅海峰的专栏订阅

在这篇文章中:

  • 1. 背景介绍
    • 1.1 无处不在的JSON
    • 1.2 令人头疼的数据校验
    • 1.3 用Django-Form校验数据会是什么样呢
    • 1.4 当前较为流行的RPC框架的解决方案——Data Model
  • 2. 模式探索——建立数据校验的基石
  • 3. 一个简易的数据校验算法实现
  • 4. JSON-Schema
  • 5. 应用分析

本文尝试通过json数据校验方法解决如下几个问题:

  • 数据没有校验,系统处于裸奔状态,导致后期维护成本高;
  • 编写一堆校验代码,混杂在业务代码中,导致代码可读性降低;
  • API交付的时候提供一大段接口描述文档,但用户还是要揣测文档意思。

1. 背景介绍

1.1 无处不在的JSON

JSON是一种轻量级的数据交换格式,基于Javascript的一个子集, 但采用完全独立于语言的文本格式,易于人阅读和编写,同时也易于机器解析和生成。这些特性使JSON成为理想的数据交换语言, 几乎所有与网页开发相关的语言都有JSON库。目前蓝鲸ESB,甚至公司内绝大多数系统的交互都采用JSON格式。

1.2 令人头疼的数据校验

由于JSON比较灵活,没有固定的schema,使用JSON作为数据交换格式时,我们经常遇到数据校验的问题。一个简单的JSON数据往往需要写一大段代码来校验数据格式是否符合预期,导致代码膨胀,可读性不好。

如下是一段CC系统新增自定义变量的请求参数,大致分成几部分请求账户、操作者、添加到的目标业务和环境类型,最后是要添加的变量列表。

{
    "systemId": "",  # 系统账号
    "password": "",  # 系统密码
    "operator": "hoffer",  # 操作者
    "ticket": "",  # 操作者的ticket信息
    "ApplicationID": 295,  # 业务ID
    "EnviType": 1,  # 环境类型
    "Params": [{
        "Scope": "[0-1000].gameserver.*.*",  # 变量作用域
        "KeyName": "domain",  # 变量名称
        "ValName": "awx.zhunter.com"  # 变量值
    },
    {
        "Scope": "[1001-2000].gameserver.*.*",  # 变量作用域
        "KeyName": "domain",  # 变量名称
        "ValName": "awx.zhunter.com"  # 变量值
    }]
}

为了校验参数的正确性,往往的做法是写如下一段代码(用kwargs表示请求参数)

# 参数数据类型校验
if not isinstance(kwargs, dict):
    return False, "kwargs must be dict"

# 校验systemId是否在参数字段中
if "systemId" not in kwargs:
    return False, "systemId is required"
# 校验systemId值的类型
if not instance(kwargs["systemId"], basestring):
    return False, "systemId must be string"
# 校验systemId值是否为空
if not kwargs["systemId"]:
    return False, "systemId can't be empty"

# 校验Params参数字段
if "Params" not in kwargs:
    return False, "Params is required"
if not isinstance(kwargs["Params"], list):
    return False, "Params must be list"

for v in kwargs["Params"]:
    if not isinstance(v, dict):
        return False, "variable must be dict"
...

完整的将校验代码写下来需要极大的耐心,校验代码很简单,但是又不太好复用,当耐心消耗殆尽的时候,我们就开始铤而走险了,先不去做校验(其实我们都明白这有多不好)。 如果不进行数据校验,系统相当于裸奔的状态,随时可能出问题,尤其是出现偶发性的数据异常时,往往排查难度非常大,如果异常发生在一个逻辑复杂的功能模块中,问题定位花的时间差不多能赶上代码编写的时间了。第三方api接口格式的变更,如果没能及时通知到调用方,也会导致潜在的风险。

1.3 用Django-Form校验数据会是什么样呢

如果用django的form做校验,代码会少点,下面是用django-form对案例数据编写的校验函数:

def validate(kwargs):
    class L1Form(forms.Form):
        systemId = forms.CharField()
        password = forms.CharField()
        operator = forms.CharField()
        ticket = forms.CharField(required=False)
        ApplicationID = forms.IntegerField(min_value=1)
        EnviType = forms.ChoiceField(choices=(1, 2, 3))

    class VariableForm(forms.Form):
        Scope = forms.RegexField(regex="^(\w+|\*|\[[,\d]+\]){3}(\w+|\*|\[[,\d]+\])$")
        KeyName = forms.CharField()
        ValName = forms.CharField()

    l1form = L1Form(data=kwargs)
    if not isinstance(kwargs, dict):
        return False, "kwargs must be dict"

    if not l1form.is_valid():
        return False, l1form.errors

    if "Params" not in kwargs:
        return False, ""

    if not isinstance(kwargs["Params"], list):
        return "Params must be list"

    for variable in kwargs["Params"]:
    if not isinstance(variable, dict):
        return False, "variable must be dict"
        vf = VariableForm(data=variable)
        if not vf.is_valid():
            return False, vf.errors

    return True, "success"

咋看之下,代码要简单很多了,用一个django-form可以把一层的简单数据类型都校验了,但仔细看看剩下的代码,会发现几个问题:

  • 用form表单校验首先需要保证数据是dict类型
  • 循环结构需要单独编写代码实现
  • 层次关系用form校验不了,如果json层次很深,校验代码就退化到了直接编码校验

1.4 当前较为流行的RPC框架的解决方案——Data Model

回想thrift作为目前较为流行的一个跨语言开发框架,使用起来就不需要这么繁杂的参数校验,究其原因是因为thrift在接口定义的时候严格定义好了接口的输入输出参数及其类型。 Google的Protocol Buffer也是需要编写一个 proto 文件,定义程序中需要处理的结构化数据。可见,为了提供可靠的数据,得先有关于数据格式的描述(数据模式),如果对json数据校验的时候,先整理出数据模式,是否也能写个通用的检验算法,运用模式对数据进行校验呢?

2. 模式探索——建立数据校验的基石

JSON作为javascript的一个子集,支持的数据类型也是可枚举的,基本数据类型有string/number/boolean/null, 容器类型由array和map。容器中容纳的元素是基本数据类型或容器,因此我们只需校验基本数据类型和对容器的结构进行校验,容器中的元素可以采用递归的方式进行校验。 由于基本的json数据以key-value的形式存在,可以针对各个字段指定应该满足的规则,形式如

"key": {
    "rule1_name": rule1_value,
    "rule2_name": rule2_value,
    ...
}

基本数据类型比较好校验,可以单独定义一些规则用于支持,比如字符串类型,可以定义一个规则名stringMaxLength指定最大长度, 定义规则名stringFormat指定其格式, 甚至定义规则stringPattern指定字符串应该遵循的正则规则。

"key": {
    "type": "string",
    "stringMaxLength": 100,
    "stringMinLength": 1,
    "stringFormat": "ipv4"
    "stringPattern": ...
}

容易类型的其实也可以按照规则-规则值的方式来指定校验规则,容器内的元素用递归的方式指定校验规则。比如一个map格式的校验规则就可以写成:

{
    "type": "map",
    "minKeys": 1,
    "maxKeys": 3,
    "requiredKeys": ["ApplicationID", "EnviType"]
}

另外还需要有字段专门用来指定容器内的元素及其对应的校验规则,可以用keys记录

{
    "type": "map",
    "minKeys": 1,
    "maxKeys": 3,
    "requiredKeys": ["ApplicationID", "EnviType"],
    "keys": {
        "key1_name": rule_for_key1,
        "key2_name": rule_for_key2,
    }
}

3. 一个简易的数据校验算法实现

如下代码就是基于上述数据格式定义规则实现的校验算法:

class ValidateError(Exception):
    pass


def base_type_validate(data, validate_type):
    if validate_type == "string":
        if not isinstance(data, basestring):
            raise ValidateError("data %s is not type %s" % (data, validate_type))
    elif validate_type == "number":
        if not isinstance(data, (int, float)):
            raise ValidateError("data %s is not type %s" % (data, validate_type))
    else:
        pass  # TODO


def stringMaxLength(s, max_length):
    # 只对string类型进行校验,保证长度不超过max_length
    if isinstance(s, basestring):
        if len(s) > max_length:
            raise ValueError("string length exceed max length %s" % max_length)


def validate_data(data, schema):
    """
    JSON格式数据校验
    :param data:
    :param schema:
    :return:
    """
    for rule, v in schema:
        if rule == "type":
            base_type_validate(data, v)
        elif rule == "stringMaxLength":
            stringMaxLength(data, v)
        elif rule == "keys":
            for _key, _schema in v:
                validate_data(_key, _schema)
        else:
            pass  # TODO

4. JSON-Schema

稍微看下上述校验算法,会发现原来实现一个通用的校验规则其实挺简单。目前python开源社区已经有了基于这种方式校验工具JSON-Schema, 其官方文档 中提供了相对完备的数据校验规则以及更好的使用体验。比如JSON-Schema提供了anyOf, allOf, oneOf, not组合规则方便我们组合出更严格的校验规则,另外还提供了definitions方式命名一套复杂的校验方案,使用时用$ref引用这个命名的校验方案(数据模式复用^_^)。更多关于json数据校验的特性还请大致浏览一遍官方文档。

使用JSON-Schema对本文开始提供的例子定义的校验模式为:

{
    "type": "object",
    "required": ["systemId", "password", "operator", "ApplicationID", "EnviType", "Params"]
    "properties": {
        "systemId": { "type": "string", "minLength": 1},
        "password": { "type": "string"},
        "operator": { "type": "string", "minLength": 1},
        "ApplicationID": { "type": "int"},
        "EnviType": { "type": "int", "enum": [1, 2, 3]},
        "Params": {
            "type": "array",
            "items": {
                "type": "object",
                "required": ["Scope", "KeyName", "ValName"],
                "properties": {
                    "Scope": {
                        "type": "string",
                        "pattern": "^(\w+|\*|\[[,\d]+\]){3}(\w+|\*|\[[,\d]+\])$"
                    },
                    "KeyName": {"type": "string", "minLength": 1},
                    "ValName": {"type": "string"},
                }
            }
        },
    }
}

5. 应用分析

最后,回过头来总结一下用JSON—Schema有哪些好处:

  1. 在输入输出的地方做参数校验,将非法输入拦截在入口, 将数据校验逻辑从业务逻辑中分离开来
    • 用户数据校验,用户无论是从前端还是API提交过来的数据,如果能通过校验发现参数问题,给用户明确提示的同时,也可以避免低效沟通
    • 入口数据校验保证数据准确性,将可以保证逻辑代码尽量精简,不需要对非法输入进行处理
    • 第三方接口提供的数据服务,并不总是可靠,将这种无效的数据拦截在系统之外。可以减少不必要的bug定位
  2. 作为API描述语言使用 当我们的系统作为平台给应用方提供API接口的时候,往往需要写上一大段接口文档,描述哪些字段是必填的,应该怎么填,用户仔细阅读完文档可能还是不知道参数是怎么填,即使提供了demo也很难覆盖全面,运用JSON-Schema定义JSON数据模式正像用数学符号表达数学问题一样,非常简洁,但是又能准确表达意思。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于 2017-08-29

Python

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值