转载:https://cloud.tencent.com/developer/article/1005810
本文尝试通过json数据校验方法解决如下几个问题:
JSON是一种轻量级的数据交换格式,基于Javascript的一个子集, 但采用完全独立于语言的文本格式,易于人阅读和编写,同时也易于机器解析和生成。这些特性使JSON成为理想的数据交换语言, 几乎所有与网页开发相关的语言都有JSON库。目前蓝鲸ESB,甚至公司内绝大多数系统的交互都采用JSON格式。
由于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接口格式的变更,如果没能及时通知到调用方,也会导致潜在的风险。
如果用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可以把一层的简单数据类型都校验了,但仔细看看剩下的代码,会发现几个问题:
回想thrift作为目前较为流行的一个跨语言开发框架,使用起来就不需要这么繁杂的参数校验,究其原因是因为thrift在接口定义的时候严格定义好了接口的输入输出参数及其类型。 Google的Protocol Buffer也是需要编写一个 proto 文件,定义程序中需要处理的结构化数据。可见,为了提供可靠的数据,得先有关于数据格式的描述(数据模式),如果对json数据校验的时候,先整理出数据模式,是否也能写个通用的检验算法,运用模式对数据进行校验呢?
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, } }
如下代码就是基于上述数据格式定义规则实现的校验算法:
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
稍微看下上述校验算法,会发现原来实现一个通用的校验规则其实挺简单。目前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"}, } } }, } }
最后,回过头来总结一下用JSON—Schema有哪些好处:
原创声明,本文系作者授权云+社区发表,未经许可,不得转载。
如有侵权,请联系 [email protected] 删除。
编辑于 2017-08-29
Python