经常用 Python 写代码的同学应该都有一个感触,那就是 Python 对于字典的支持太舒服了,而且基本上可以和 JS 中写 Json 一样舒服。但是,因为 Python 对于 Dict 的支持比较松散,所以,导致了一个问题,假如我有一个函数,参数如果放它一个字典,那调用者会疯掉的,这是一种情况;另外一个常见的场景就是参数校验,无论是 HTTP 还是 RPC 等形式,很多时候我们的参数都是以 JSON 的形式传递的,如何对这结构体进行描述或者验证都是一个比较棘手的问题。

对于 JSON 的这个问题,我关注它也有两年多的时间了,我觉得还没见到一种完美的方式,总是带着一些遗憾,但是不无办法,例如本文就介绍一种还不错的方式,但是,缺点就是语法其实还是比较复杂的,制定 Schema 的时候有阵痛,但是用起来的时候就很方便、很舒服了,这就是我在这篇文章中想介绍的一个 Python Library:JSON Schema

json schema 简介

关于 json schema 其实无需介绍过多,无非就是一个定义 json 的 schema 的 library,其中肯定包含两部分,第一部分是 Schama 的定义,这个属于文档方面的事情;另一部分就是 Schema 的使用,这是属于开发方面的问题。本文就从这两方面说起,看看 JSON-Schema 是如何处理 json 的 schema 的。

在讲关于这个 python library 之前,顺便聊一些题外话,那就是 json 的 schema 问题,在使用 json-schema 这个 Library 的时候,我被导到一个 json-schema.org 的网站上,然后这里定义了关于 json schema 的相关的规范,最新的版本是 draft-7,从这个版本来看,似乎这还不是正式版本,不过既然有规范,而且我针对我的需求,扫了几圈这个规范,发现是挺完善的,所以,不妨将这个规范用作日常使用。

json schema 定义

json-schema 这个 library 在它的描述中声明目前是完全支持:Draft3Draft4 规范的,所以我这里就以 Draft4 为规范进行 json 的 schema 描述。

先说一下在本文中我将使用的示例背景,在本文中,我将描述一个博客文章的模型,然后这个博客文章会属于一些分类分类是嵌套在文章中的,这里其实分类文章是一个多对多的关系,但是因为分类自身比较简单,所以为了简化模型,我将分类嵌入到文章模型中,所以整体模型是这样的:

依照这个模型,我可以先定义一个简单的对应的 json 序列:

接着我们就可以先使用 json-schema 来定义一下我们有哪些 Field,注意,这里我们先不做其他方面的定义进行定义,而只是先看看需要什么 Field,那么一个非常简单的 Schema 就出来了,像这样:

各要素都比较完备了,简单明了的说明了我们的 post 的数据结构,下一步就是定义一下各个字段的类型了。

json schema 类型

在 json schema 中,描述类型都是通过 K-V 的形式进行描述的,并且 Key 肯定是 "type",然后 value 就是对应的类型,这个对应的类型不是随便写的,只能从下面这 6 个中选择:

所以我们就可以先填充一下我们的 Schema:

ok,现在看上去这个 Schema 已经有点成效了,但是,对于一些字段我们可能还需要进行更多的限制,例如 status,我们不能让别人随意的发挥,所以自然是需要一些 ENUM 值进行选择,所以也需要制定以下。

json schema 属性

在 json schema 中,字段除了类型,还可以添加一些属性,常用的属性有

这里就给我们的 schema 加上一些限制:

似乎更好一些了,然后我们看向了 categories,觉得这个只指定了类型为 array,但是 array 里面的元素应该是什么类型却没有说,所以我们还是需要指定一下的。

json schema 数组

在 json-schema 中,如果要定义数组的元素类型,那么可以通过 items 属性定义,定义的方式就和定义一个对象一致,因此,我们就可以这么搞了:

这样一定义,我们的数组 schema 就完整多了,但是,从这里我们就可以发现,这里的 分类 Model 还比较简单,如果更复杂一些,那么 Post 的 Schema 就比较难看了,有没有一些更好的方式可以简化这个 schema。

json schema 引用

在 json-schema 中,我们经常会有一个 Object 中包含另外一个 Object 的情况,对于这种情况,当然可以在 Object 定义其他 Object,但是,正如前面所说,这样太复杂了,所以,为了更加简单清晰得描述关系,在 json-schema 中支持 reference,使用方式为在 Object 中使用关键字 $ref,然后值为对应 Object 的定义,需要注意的是,这个 Object 的定义需要在同个 Object 的 definitions 的 value 中,示例为:

这样,对于 Post 这个 Schema 就清晰多了。

这些就是 json-schema 一些常见的用法,通过这些用法的组合,足以让我们应对大部分的场景。如果当你遇到应对不了的场景的时候,别慌,可以去看看 schema 规范:json-schema,这份规范不复杂,可以较快得找到你需要的内容。

json schema 校验

当我们把 schema 定义好了之后,是时候尝试在代码中使用一把了,我这里使用的还是 Python 编程语言,这里我不能说使用任何编程语言都可以,根据我查找过程中的一些观察,似乎除了 Python,还有 Java 我是明确知道支持的,至于其他语言我就不太清楚了。

在 Python 中,首先第一步我们肯定是离不开安装依赖的 Library 的,肯定是老方法,直接用 pip 安装了,有简单的方式为什么不用呢:

$ pip install jsonschema

然后使用过程也是极其简单,下面这是一个针对我们刚才的 schema 的验证代码,可以一直执行尝试一下:

然后执行一遍看看,结果肯定是:

$ json is ok!

假设我们修改一下 json,看看不 ok 的时候又是什么情况?如果你试了,你肯定会发现不会输出 "json is invalid!" 这个错误,反而是直接抛出了异常,但是异常的内容又很多,非常得详细,有时我们不需要知道这么多,所以为了简略处理,我们需要对 validate 这个函数有所了解,这里对函数原型做一个简单的观看:

这里讲了可能抛出两种异常,分别是 :

所以我们在做开发调试的时候需要注意一些,如果是我们的 Schema 都编写错了,那么无论验证什么 json 必然都是会抛出异常的,这个坑是值得注意的。然后我们再看下 ValidationError 这个异常有啥内容:

可以发现其实这两种异常都是相同的属性,所以我们将他们的内容都打印出来看看:

message: 'hello' is not one of ['PostStatus.PUBLISHED', 'PostStatus.DELETED', 'PostStatus.DRAFT']
path: deque(['status'])
cause: None
context: []
instance: hello
schema: {'enum': ['PostStatus.PUBLISHED', 'PostStatus.DELETED', 'PostStatus.DRAFT'], 'type': 'string'}
schema_path: deque(['properties', 'status', 'enum'])
parent: None

ok,这样我们就很清楚每个字段表示的含义了,同时就可以在开发的时候根据自己的需要使用对应的字段了,一般情况下我建议直接使用 message 就可以了,所以最后示例代码修改成了:

总结

在本文中,我尝试以一种比较简单的方式对 json 的 schema 定义进行了一个介绍,同时,也一步步得对一个简单的示例进行扩展,最后使用 Python 编程语言对这个 Schema 进行验证。json 的使用在日常的开发中极其常见,而且因为其不错的灵活性,给我们开发带来了很大的方便;但是,在享受方便的同时,也经常给我们带来的意想不到的 BUG,所以,对于一些比较重要的出入口,不妨加上一个 Schema 校验,让程序运行得更健壮些。