最近作毕设的时候,有个功能须要验证JSON字符串的合法性,最简单的作法是直接用Go的第三方JSON库作一个反序列化,但这样作给我有种杀鸡用牛刀的感受,毕竟我不须要真正的反序列化它,单纯的验证和反序列化的性能差距直观感受上仍是比较大的。
既然要验证JSON的有效性,那么必然须要清楚的知道JSON格式,这个在JSON官网已经给咱们画出来了:git
从官方的图上面能够看出,JSON的组成一共有五部分:github
- object: 以左大括号({)开头表示对象的开始。
- array: 以左中括号([)开头表示数组的开始。
- value: 数组中只能有值类型,对象中每个键后面必跟一个值类型。
- string: 以英文的双引号开头表示字符串类型。
- number: 以减号(-)、1~九、0开头表示数值类型。
从上能够看出,每一种不一样的类型均可以用不一样的字符来标识
,且根据这个特定的符号转移到不一样类型的解析状态,显然实际上就是一个状态机,而这个状态机只须要处理五种不一样类型的解析便可。json
咱们须要先定义一些常量来标识每一个特定字符所表明的意义, 大多数常量的定义和上面的图中一一对应:数组
const ( OBJ_START = '{' // 标识指望一个object解析开始 OBJ_END = '}' // 标识指望一个object解析结束 ARR_START = '[' // 标识指望一个array解析开始 ARR_END = ']' // 标识指望一个array解析结束 SEP_COLON = ':' // 标识指望一个键值对的value SEP_COMMA = ',' // 标识指望下一个键值对或者下一个value BOOL_T = 't' // 标识指望一个true BOOL_F = 'f' // 标识指望一个false NULL_START = 'n' // 标识指望一个null CONTROL_CHARACTER = 0x20 // JSON中0x20如下的控制字符是不容许出现的 ) const ( REVERSE_SOLIDUS = '\\' // 标识转义字符,指望接下去读的字符是反斜杠或如下8个字符中的一个, QUOTATION_MARK = '"' SOLIDUS = '/' BACKSPACE = 'b' FORMFEED = 'f' NEWLINE = 'n' CARRIAGE_RETURN = 'r' HORIZONTAL_TAB = 't' FOUR_HEXADECIMAL_DIGITS = 'u' ) const ( NUMBER_DOT = '.' NUMBER_e = 'e' NUMBER_E = 'E' NUMBER_PLUS = '+' NUMBER_MINUS = '-' NUMBER_ZERO = '0' )
将解析过程当中出现的错误简单分红三种类型,并封装错误信息:数据结构
var ( ErrInvalidJSON = errors.New("invalid json format") ErrUnexpectedEOF = errors.New("unexpected end of JSON") ErrStringEscape = errors.New("get an invalid escape character") ) type ErrJSON struct { err error // 标识错误的类型 additional string // 描述错误具体信息 part string // 从解析错误的那个字符开始的一部分json字符串 } func (e ErrJSON) Error() string { return e.String() } func (e ErrJSON) String() string { return fmt.Sprintf("error:\n\t%s\nadditional:\n\t%s\n"+ "occur at:\n\t %s\n", e.err, e.additional, e.part) }
将JSON字节切片封装一下,每次读取第X个字符或移动X个字符时都须要第本次操做的有效性用validateLen
方法验证。函数
jsonBytes是原始JSON字符串转换成的切片表示,而且每次moveX后都会从新切片:jsonBytes = jsonBytes[...]
。
maxPosition是jsonBytes的最初长度,即:len(jsonBytes)
。
position是当前读取到的位置。
type JSON struct { jsonBytes []byte position uint maxPosition uint } func (j *JSON) len() int { return len(j.jsonBytes) } func (j *JSON) validateLen(x uint) { if j.maxPosition <= j.position { panic(ErrJSON{ err: ErrUnexpectedEOF, part: getPartOfJSON(j), }) } } func (j *JSON) moveX(x uint) *JSON { if x == 0 { return j } j.validateLen(x) j.jsonBytes = j.jsonBytes[x:] j.position += x return j } func (j *JSON) moveOne() *JSON { return j.moveX(1) } func (j *JSON) byteX(x uint) byte { j.validateLen(x) return j.jsonBytes[x] } func (j *JSON) firstByte() byte { return j.byteX(0) }
在JSON中,空格、回车、制表符等在非字符串中是会被直接忽略的
,因此每次读取一个字节后都须要去除剩余字节数组中前面那部分的空白字节,由于读取只会是从左往右的,因此不必浪费cpu在去除右侧的空白字符:性能
func TrimLeftSpace(data *JSON) *JSON { for idx, r := range data.jsonBytes { // 调用unicode包的IsSpace函数判断是不是空白字符便可 if !unicode.IsSpace(rune(r)) { return data.moveX(uint(idx)) } } return data.moveX(uint(data.len())) }
在有错误发生时,咱们但愿不只得到是什么样的错误,还但愿能获得从错误发生的那个字符开始的一部分JSON字符串,方便定位错误发生的位置,getPartOfJSON函数会返回从错误开始发生处的接下去40个字符的字符串:ui
func getPartOfJSON(data *JSON) string { return string([]rune(string(data.jsonBytes[:160]))[:40]) }
有了这个函数,再加上上面对错误信息的封装,接下去只要遇到解析错误,就能够直接调用这样的panic:spa
panic(ErrJSON{ err: ErrInvalidJSON, additional: "expect a null value: null", part: getPartOfJSON(data), })
咱们还须要这样一个函数,它用来判断JSON.jsonBytes中的第一个字节是否和目标字节相等
,若是不相等则直接触发ErrInvalidJSON
,这个函数是很是有用的,用在如下几个地方:3d
- 在验证object时,JSON.jsonBytes中的第一个字符必须是左大括号(
{
) -> Expect(OBJ_START, data)- 在验证object时,key验证完后必须紧跟着一个英文下的冒号(
:
) -> Expect(SEP_COLON, TrimLeftSpace(data))- 在验证string时,JSON.jsonBytes中的第一个字符必须是英文下的双引号(
"
) -> Expect(QUOTATION_MARK, data)- 在验证array时,JSON.jsonBytes中的第一个字符必须是左中括号(
[
) -> Expect(ARR_START, data)
func Expect(b byte, data *JSON) { if data.firstByte() != b { panic(ErrJSON{ err: ErrInvalidJSON, additional: fmt.Sprintf("expect character: %c", b), part: getPartOfJSON(data), }) } TrimLeftSpace(data.moveOne()) return }
有了以上封装的数据结构和辅助函数,接下去就能够开始编写各个验证函数了,首先是入口函数Validate
。
JSON字符串的根节点只能是两种类型的数据: object或array
,所以若是不是以 {
或者 [
开头,则认为是非法JSON字符串。而且在验证完以后若是还有其余非空白字符,也认为是非法JSON字符串,由于JSON中只容许有一个根节点。:
func Validate(jsonStr string) (err error) { defer func() { if e := recover(); e != nil { if e, ok := e.(error); ok { err = e.(error) } else { panic(e) } } }() data := &JSON{[]byte(jsonStr), 0, uint(len(jsonStr))} TrimLeftSpace(data) if data.firstByte() == OBJ_START { ValidateObj(data) if TrimLeftSpace(data).len() == 0 { return nil } } else if data.firstByte() == ARR_START { ValidateArr(data) if TrimLeftSpace(data).len() == 0 { return nil } } return ErrJSON{ err: ErrInvalidJSON, additional: "extra characters after parsing", part: getPartOfJSON(data), } }
根据object组成,咱们的验证流程以下:
- 第一个字符是不是
{
。- 是不是一个空对象
{}
,若是是则跳过}
并返回。按照如下流程循环验证键值对:
- 验证key是不是合法字符串。
- key验证结束后,必须有一个
:
。- 验证一个value类型。
一个键值对验证完成后只会存在两种状况:
- 紧跟着一个
,
代表指望有下一个键值对,这种状况下循环继续。- 紧跟着一个
}
标识这个object类型验证结束,跳过'}'符号并返回。
func ValidateObj(data *JSON) { Expect(OBJ_START, data) if TrimLeftSpace(data).firstByte() == OBJ_END { data.moveOne() return } for { ValidateStr(TrimLeftSpace(data)) Expect(SEP_COLON, TrimLeftSpace(data)) ValidateValue(TrimLeftSpace(data)) TrimLeftSpace(data) if data.firstByte() == SEP_COMMA { data.moveOne() } else if data.firstByte() == OBJ_END { data.moveOne() return } else { panic(ErrJSON{ err: ErrInvalidJSON, additional: `expect any one of the following characters: ',' '}'`, part: getPartOfJSON(data), }) } } }
array的组成和验证流程比object要简单一些,由于array中没有key只有value,验证流程以下:
- 第一个字符是不是
[
。- 是不是一个空数组
[]
,若是是则跳过]
并返回。按照如下流程循环验证array中的value:
- 验证是不是一个合法的value。
一个value验证完成后只会存在两种状况:
- 紧跟着一个
,
代表指望有下一个value,这种状况下循环继续。- 紧跟着一个
]
标识这个array类型验证结束,跳过']'符号并返回。
func ValidateArr(data *JSON) { Expect(ARR_START, data) if TrimLeftSpace(data).firstByte() == ARR_END { data.moveOne() return } for { ValidateValue(TrimLeftSpace(data)) TrimLeftSpace(data) if data.firstByte() == SEP_COMMA { data.moveOne() } else if data.firstByte() == ARR_END { data.moveOne() return } else { panic(ErrJSON{ err: ErrInvalidJSON, additional: `expect any one of the following characters: ',' ']'`, part: getPartOfJSON(data), }) } } }
string的验证相对array和object要复杂一点,分红两个函数,一个是验证字符串的主体函数ValidateStr
,一个是验证转义字符ValidateEsc
,
验证流程以下:
- 第一个字符是不是
"
。按照如下流程循环验证字符串中的每个字符:
- 先判断needEsc是否为true,
needEsc只有在前一个字符是反斜杠(\)的状况下为true
,若是为true则调用ValidateEsc
函数验证转义字符的合法性,并在验证经过后置needEsc为false。若是needEsc为false,则按照如下流程验证:
- 若是当前字符是
"
,则表示字符串验证结束,跳过idx个字符并返回。- 若是当前字符是
\
,则置needEsc位true表示下一个字符指望是转义字符。- 若是当前字符是
控制字符( < 0x20 )
,则触发panic,由于string中不容许出现控制字符。- 若是上述三种状况都不是,则表明是一些合法的容许出如今string中的普通字符,直接跳过该字符。
- 若是for循环结束,则该JSON字符串必是非法的,由于JSON不可能以string开始也不可能以string结束。
func ValidateStr(data *JSON) { Expect(QUOTATION_MARK, data) var needEsc bool RE_VALID: for idx, r := range data.jsonBytes { if needEsc { ValidateEsc(data.moveX(uint(idx))) needEsc = false goto RE_VALID } switch { case r == QUOTATION_MARK: data.moveX(uint(idx + 1)) return case r == REVERSE_SOLIDUS: needEsc = true case r < CONTROL_CHARACTER: panic(ErrJSON{ err: ErrInvalidJSON, additional: "control characters are not allowed in string type(< 0x20)", part: getPartOfJSON(data), }) } } panic(ErrJSON{ err: ErrUnexpectedEOF, part: getPartOfJSON(data), }) }
ValidateEsc
函数很简单,只有两种状况:
- 当前字符是不是
"
、\
、/
、b
、f
、n
、r
、t
中的一个,若是是的话则跳过当前字符并返回。当前字符是不是
u
,若是是则继续如下验证:
- 验证接下去的4个字符是不是十六进制的表示,即在
范围0~九、A~F、a~f
中,若是是,则是合法转义字符,不然是非法的转义字符。若是以上两种都不是的话,则当前字符不符合JSON中转义字符的定义,认为是非法JSON字符串。
func ValidateEsc(data *JSON) { switch data.firstByte() { case QUOTATION_MARK, REVERSE_SOLIDUS, SOLIDUS, BACKSPACE, FORMFEED, NEWLINE, CARRIAGE_RETURN, HORIZONTAL_TAB: TrimLeftSpace(data.moveOne()) return case FOUR_HEXADECIMAL_DIGITS: for i := 1; i <= 4; i++ { switch { case data.byteX(uint(i)) >= '0' && data.byteX(uint(i)) <= '9': case data.byteX(uint(i)) >= 'A' && data.byteX(uint(i)) <= 'F': case data.byteX(uint(i)) >= 'a' && data.byteX(uint(i)) <= 'f': default: panic(ErrJSON{ err: ErrStringEscape, additional: `expect to get unicode characters consisting of \u and 4 hexadecimal digits`, part: getPartOfJSON(data), }) } } TrimLeftSpace(data.moveX(5)) default: panic(ErrJSON{ err: ErrStringEscape, additional: `expect to get unicode characters consisting of \u and 4 hexadecimal digits, or any one of the following characters: '"' '\' '/' 'b' 'f' 'n' 'r' 't'`, part: getPartOfJSON(data), }) } return }
根据valuye的组成,咱们的验证流程以下:
- 第一个字符是不是
"
,是的话代表该value是一个string,调用ValidateStr验证string。- 第一个字符是不是
{
,是的话代表该value是一个object,调用ValidateObj验证object。- 第一个字符是不是
[
,是的话代表该value是一个array,调用ValidateArr验证array。- 第一个字符是不是
t
,是的话代表该value是true
,验证接下去的三个字符是否分别为r
、u
、e
,若是是的话跳过true这四个字符并返回,不然触发panic。- 第一个字符是不是
f
,是的话代表该value是false
,验证接下去的四个字符是否分别为a
、l
、s
、e
,若是是的话跳过false这五个字符并返回,不然触发panic。- 第一个字符是不是
n
,是的话代表该value是null,验证接下去的三个字符是否分别位u
、l
、l
,若是是的话跳过null这四个字符并返回,不然触发panic。- 第一个字符是不是
0
、-
或者在字符1
~9
之间,是的话代表该value是一个number类型,调用ValidateNumber验证number。- 若是以上7种状况都不是的话,则该JSON字符串是不合法的,触发panic。
func ValidateValue(data *JSON) { b := data.firstByte() switch { case b == QUOTATION_MARK: ValidateStr(data) case b == OBJ_START: ValidateObj(data) case b == ARR_START: ValidateArr(data) case b == BOOL_T: if data.byteX(1) != 'r' || data.byteX(2) != 'u' || data.byteX(3) != 'e' { panic(ErrJSON{ err: ErrInvalidJSON, additional: "expect a bool value: true", part: getPartOfJSON(data), }) } data.moveX(4) case b == BOOL_F: if data.byteX(1) != 'a' || data.byteX(2) != 'l' || data.byteX(3) != 's' || data.byteX(4) != 'e' { panic(ErrJSON{ err: ErrInvalidJSON, additional: "expect a bool value: false", part: getPartOfJSON(data), }) } data.moveX(5) case b == NULL_START: if data.byteX(1) != 'u' || data.byteX(2) != 'l' || data.byteX(3) != 'l' { panic(ErrJSON{ err: ErrInvalidJSON, additional: "expect a null value: null", part: getPartOfJSON(data), }) } data.moveX(4) case b == NUMBER_MINUS || b == NUMBER_ZERO || (b >= '1' && b <= '9'): ValidateNumber(data) default: panic(ErrJSON{ err: ErrInvalidJSON, additional: `expect any one of the following characters: '"' '{' '[' 't' 'f' 'n' '-' '0' '1' '2' '3' '4' '5' '6' '7' '8' '9'`, part: getPartOfJSON(data), }) } return }
number的验证相对是最复杂的(其实也不复杂,就是判断多了一点),一样分红两个函数,一个是验证number的主体函数ValidateNumber
,一个是验证连续整数的函数ValidateDigit
,
验证流程以下:
- 第一个字符是不是
-
,若是是则跳过该字符。接着分红两种状况:
- 第一个字符是不是0,若是是的跳过该字符。
- 第一个字符是否在字符
1
~9
之间,若是是的话跳过该字符并调用ValidateDigit函数验证一串连续的整数。- 若是以上两种都不是的话,则该JSON字符串非法,当前字符不符合number的组成格式。
- 经过前面的两个验证后,接下去是否跟着一个
.
若是是的话继续验证小数部分,即调用ValidateDigit验证一串连续的整数。- 接着验证是否跟着
e
或者E
,是的话继续验证科学计数法的表示,不然number类型验证结束,直接return。- 验证是否紧跟着
+
或者-
,是的话跳过该字符- 调用ValidateDigit验证一串连续整数。
func ValidateNumber(data *JSON) { if data.firstByte() == NUMBER_MINUS { data.moveOne() } if data.firstByte() == NUMBER_ZERO { data.moveOne() // do nothing, maybe need read continuous '0' character } else if data.firstByte() >= '1' || data.firstByte() <= '9' { data.moveOne() if data.firstByte() >= '0' && data.firstByte() <= '9' { ValidateDigit(data) } } else { panic(ErrJSON{ err: ErrInvalidJSON, additional: `expect any one of the following characters: '-' '0' '1' '2' '3' '4' '5' '6' '7' '8' '9'`, part: getPartOfJSON(data), }) } if data.firstByte() == NUMBER_DOT { ValidateDigit(data.moveOne()) } if data.firstByte() != NUMBER_e && data.firstByte() != NUMBER_E { return } data.moveOne() if data.firstByte() == NUMBER_PLUS || data.firstByte() == NUMBER_MINUS { data.moveOne() } ValidateDigit(data) return }
ValidateDigit函数会尝试读取一串连续的范围在0
~9
之间的字符,直到遇到不在范围内的字符为止,若是for循环结束还没return的话,则当前JSON字符串必是非法字符串,觉得JSON不可能以整开头也不可能以整数结尾。
func ValidateDigit(data *JSON) { if data.firstByte() < '0' || data.firstByte() > '9' { panic(ErrJSON{ err: ErrInvalidJSON, additional: "expect any one of the following characters: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9'", part: getPartOfJSON(data), }) } data.moveOne() for idx, b := range data.jsonBytes { if b < '0' || b > '9' { data.moveX(uint(idx)) return } } panic(ErrJSON{ err: ErrUnexpectedEOF, part: getPartOfJSON(data), }) }
JSON字符串的验证比想象中的要简单不少,能够说是至关的简单,这得益于在官网上已经将各个状态的扭转、格式类型和组成图给你画好了,只要代码没写错,照着图把各个部分的验证写出来就实现了。
在写完后,我用fastjson的issue859.json测了一下性能,和调用Go的json库或其它三方json库相比,这个实现的性能要高出30%左右,所以若是有需求只验证不解析的,花点时间手撸一个验证器仍是很划算的。
完整代码能够在这里找到
若是文章有什么问题,或者有其它更好的想法,欢迎留言或私信交流。
转载请注明出处: 动手实现一个JSON验证器