动手实现一个JSON验证器(上)
分析
既然要验证JSON的有效性,那么必然需要清楚的知道JSON格式,这个在JSON官网已经给我们画出来了:
从官方的图上面可以看出,JSON的组成一共有五部分:
- object: 以左大括号({)开头表示对象的开始。
- array: 以左中括号([)开头表示数组的开始。
- value: 数组中只能有值类型,对象中每一个键后面必跟一个值类型。
- string: 以英文的双引号开头表示字符串类型。
- number: 以减号(-)、1~9、0开头表示数值类型。
从上可以看出,每一种不同的类型都可以用不同的字符来标识
,且根据这个特定的符号转移到不同类型的解析状态,显然实际上就是一个状态机,而这个状态机只需要处理五种不同类型的解析即可。
实现
常量定义
我们需要先定义一些常量来标识每个特定字符所代表的意义, 大多数常量的定义和上面的图中一一对应:
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:nt%snadditional:nt%sn"+ "occur at:nt %sn", e.err, e.additional, e.part)
}
JSON字节切片封装
将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字符串中的一部分
在有错误发生时,我们希望不仅获得是什么样的错误,还希望能得到从错误发生的那个字符开始的一部分JSON字符串,方便定位错误发生的位置,getPartOfJSON函数会返回从错误开始发生处的接下去40个字符的字符串:
func getPartOfJSON(data *JSON) string { return string([]rune(string(data.jsonBytes[:160]))[:40])
}
有了这个函数,再加上上面对错误信息的封装,接下去只要遇到解析错误,就可以直接调用这样的panic:
panic(ErrJSON{ err: ErrInvalidJSON,
additional: "expect a null value: null",
part: getPartOfJSON(data),})
Expect函数
我们还需要这样一个函数,它用来判断JSON.jsonBytes中的第一个字节是否和目标字节相等
,如果不相等则直接触发ErrInvalidJSON
,这个函数是非常有用的,用在以下几个地方:
- 在验证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
根据object组成,我们的验证流程如下:
- 第一个字符是否是
{
。 - 是否是一个空对象
{}
,如果是则跳过}
并返回。 - 按照以下流程循环验证键值对:
- 紧跟着一个
,
表明期望有下一个键值对,这种情况下循环继续。 - 紧跟着一个
}
标识这个object类型验证结束,跳过'}'符号并返回。 - 验证key是否是合法字符串。
- key验证结束后,必须有一个
:
。 - 验证一个value类型。
- 一个键值对验证完成后只会存在两种情况:
- 紧跟着一个
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
array的组成和验证流程比object要简单一些,因为array中没有key只有value,验证流程如下:
- 第一个字符是否是
[
。 - 是否是一个空数组
[]
,如果是则跳过]
并返回。 - 按照以下流程循环验证array中的value:
- 紧跟着一个
,
表明期望有下一个value,这种情况下循环继续。 - 紧跟着一个
]
标识这个array类型验证结束,跳过']'符号并返回。 - 验证是否是一个合法的value。
- 一个value验证完成后只会存在两种情况:
- 紧跟着一个
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
string的验证相对array和object要复杂一点,分成两个函数,一个是验证字符串的主体函数ValidateStr
,一个是验证转义字符ValidateEsc
,
验证流程如下:
- 第一个字符是否是
"
。 - 按照以下流程循环验证字符串中的每一个字符:
- 如果当前字符是
"
,则表示字符串验证结束,跳过idx个字符并返回。 - 如果当前字符是
,则置needEsc位true表示下一个字符期望是转义字符。
- 如果当前字符是
控制字符( < 0x20 )
,则触发panic,因为string中不允许出现控制字符。 - 如果上述三种情况都不是,则代表是一些合法的允许出现在string中的普通字符,直接跳过该字符。
- 先判断needEsc是否为true,
needEsc只有在前一个字符是反斜杠()的情况下为true
,如果为true则调用ValidateEsc
函数验证转义字符的合法性,并在验证通过后置needEsc为false。 - 如果needEsc为false,则按照以下流程验证:
- 如果当前字符是
- 如果for循环结束,则该JSON字符串必是非法的,因为JSON不可能以string开始也不可能以string结束。
func ValidateStr(data *JSON) {
Expect(QUOTATION_MARK, data) var needEsc boolRE_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),
})
}
- java中的序列化 (r4笔记第64天)
- JSP第四篇【EL表达式介绍、获取各类数据、11个内置对象、执行运算、回显数据、自定义函数、fn方法库】
- 03 整合IDEA+Maven+SSM框架的高并发的商品秒杀项目之web层
- JSP第三篇【JavaBean的介绍、JSP的行为--JavaBean】
- Java基础-06.总结二维数组,面向对象
- 04 整合IDEA+Maven+SSM框架的高并发的商品秒杀项目之高并发优化
- 过滤器第一篇【介绍、入门、简单应用】
- 通过pl/sql来格式化sql(r4笔记第63天)
- 程序员如何写出杀手级的简历
- 过滤器第二篇【编码、敏感词、压缩、转义过滤器】
- JSP第二篇【内置对象的介绍、4种属性范围、应用场景】
- Struts2的配置和一个简单的例子
- 监听器第一篇【基本概念、Servlet各个监听器】
- 监听器第二篇【统计网站人数、自定义session扫描器、踢人小案例】
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法