動手實現一個JSON驗證器

最近作畢設的時候,有個功能須要驗證JSON字符串的合法性,最簡單的作法是直接用Go的第三方JSON庫作一個反序列化,但這樣作給我有種殺雞用牛刀的感受,畢竟我不須要真正的反序列化它,單純的驗證和反序列化的性能差距直觀感受上仍是比較大的。

分析

既然要驗證JSON的有效性,那麼必然須要清楚的知道JSON格式,這個在JSON官網已經給咱們畫出來了:git

object
array
value
string
number

從官方的圖上面能夠看出,JSON的組成一共有五部分:github

  1. object: 以左大括號({)開頭表示對象的開始。
  2. array: 以左中括號([)開頭表示數組的開始。
  3. value: 數組中只能有值類型,對象中每個鍵後面必跟一個值類型。
  4. string: 以英文的雙引號開頭表示字符串類型。
  5. 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字節切片封裝

將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個字符的字符串: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),
})

Expect函數

咱們還須要這樣一個函數,它用來判斷JSON.jsonBytes中的第一個字節是否和目標字節相等,若是不相等則直接觸發ErrInvalidJSON,這個函數是很是有用的,用在如下幾個地方:3d

  1. 在驗證object時,JSON.jsonBytes中的第一個字符必須是左大括號({) -> Expect(OBJ_START, data)
  2. 在驗證object時,key驗證完後必須緊跟着一個英文下的冒號(:) -> Expect(SEP_COLON, TrimLeftSpace(data))
  3. 在驗證string時,JSON.jsonBytes中的第一個字符必須是英文下的雙引號(") -> Expect(QUOTATION_MARK, data)
  4. 在驗證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組成,咱們的驗證流程以下:

  1. 第一個字符是不是{
  2. 是不是一個空對象{},若是是則跳過}並返回。
  3. 按照如下流程循環驗證鍵值對:

    1. 驗證key是不是合法字符串。
    2. key驗證結束後,必須有一個:
    3. 驗證一個value類型。
    4. 一個鍵值對驗證完成後只會存在兩種狀況:

      1. 緊跟着一個,代表指望有下一個鍵值對,這種狀況下循環繼續。
      2. 緊跟着一個}標識這個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

array
array的組成和驗證流程比object要簡單一些,由於array中沒有key只有value,驗證流程以下:

  1. 第一個字符是不是[
  2. 是不是一個空數組[],若是是則跳過]並返回。
  3. 按照如下流程循環驗證array中的value:

    1. 驗證是不是一個合法的value。
    2. 一個value驗證完成後只會存在兩種狀況:

      1. 緊跟着一個,代表指望有下一個value,這種狀況下循環繼續。
      2. 緊跟着一個]標識這個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

string
string的驗證相對array和object要複雜一點,分紅兩個函數,一個是驗證字符串的主體函數ValidateStr,一個是驗證轉義字符ValidateEsc
驗證流程以下:

  1. 第一個字符是不是"
  2. 按照如下流程循環驗證字符串中的每個字符:

    1. 先判斷needEsc是否爲true,needEsc只有在前一個字符是反斜槓(\)的狀況下爲true,若是爲true則調用ValidateEsc函數驗證轉義字符的合法性,並在驗證經過後置needEsc爲false。
    2. 若是needEsc爲false,則按照如下流程驗證:

      1. 若是當前字符是",則表示字符串驗證結束,跳過idx個字符並返回。
      2. 若是當前字符是\,則置needEsc位true表示下一個字符指望是轉義字符。
      3. 若是當前字符是控制字符( < 0x20 ),則觸發panic,由於string中不容許出現控制字符。
      4. 若是上述三種狀況都不是,則表明是一些合法的容許出如今string中的普通字符,直接跳過該字符。
  3. 若是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函數

string
ValidateEsc函數很簡單,只有兩種狀況:

  1. 當前字符是不是"\/bfnrt中的一個,若是是的話則跳過當前字符並返回。
  2. 當前字符是不是u,若是是則繼續如下驗證:

    1. 驗證接下去的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
}

驗證value類型

value
根據valuye的組成,咱們的驗證流程以下:

  1. 第一個字符是不是",是的話代表該value是一個string,調用ValidateStr驗證string。
  2. 第一個字符是不是{,是的話代表該value是一個object,調用ValidateObj驗證object。
  3. 第一個字符是不是[,是的話代表該value是一個array,調用ValidateArr驗證array。
  4. 第一個字符是不是t,是的話代表該value是true,驗證接下去的三個字符是否分別爲rue,若是是的話跳過true這四個字符並返回,不然觸發panic。
  5. 第一個字符是不是f,是的話代表該value是false,驗證接下去的四個字符是否分別爲alse,若是是的話跳過false這五個字符並返回,不然觸發panic。
  6. 第一個字符是不是n,是的話代表該value是null,驗證接下去的三個字符是否分別位ull,若是是的話跳過null這四個字符並返回,不然觸發panic。
  7. 第一個字符是不是0-或者在字符1~9之間,是的話代表該value是一個number類型,調用ValidateNumber驗證number。
  8. 若是以上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
number的驗證相對是最複雜的(其實也不復雜,就是判斷多了一點),一樣分紅兩個函數,一個是驗證number的主體函數ValidateNumber,一個是驗證連續整數的函數ValidateDigit
驗證流程以下:

  1. 第一個字符是不是-,若是是則跳過該字符。
  2. 接着分紅兩種狀況:

    1. 第一個字符是不是0,若是是的跳過該字符。
    2. 第一個字符是否在字符19之間,若是是的話跳過該字符並調用ValidateDigit函數驗證一串連續的整數。
    3. 若是以上兩種都不是的話,則該JSON字符串非法,當前字符不符合number的組成格式。
  3. 經過前面的兩個驗證後,接下去是否跟着一個.若是是的話繼續驗證小數部分,即調用ValidateDigit驗證一串連續的整數。
  4. 接着驗證是否跟着e或者E,是的話繼續驗證科學計數法的表示,不然number類型驗證結束,直接return。
  5. 驗證是否緊跟着+或者-,是的話跳過該字符
  6. 調用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函數

ValidateDigit函數會嘗試讀取一串連續的範圍在09之間的字符,直到遇到不在範圍內的字符爲止,若是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驗證器

相關文章
相關標籤/搜索