1. 程式人生 > >go語言JSON驗證器

go語言JSON驗證器

分析

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

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

  1. object: 以左大括號({)開頭表示物件的開始。

  2. array: 以左中括號([)開頭表示陣列的開始。

  3. value: 陣列中只能有值型別,物件中每一個鍵後面必跟一個值型別。

  4. string: 以英文的雙引號開頭表示字串型別。

  5. 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:\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個字元的字串:

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,這個函式是非常有用的,用在以下幾個地方:

  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組成,我們的驗證流程如下:

  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的組成和驗證流程比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的驗證相對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函式

ValidateEsc函式很簡單,只有兩種情況:

  1. 當前字元是否是"\/bfnrt中的一個,如果是的話則跳過當前字元並返回。

  2. 當前字元是否是u,如果是則繼續以下驗證:

    1. 驗證接下去的4個字元是否是十六進位制的表示,即在範圍0~9、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型別

根據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的主體函式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%左右,因此如果有需求只驗證不解析的,花點時間手擼一個驗證器還是很划算的。
完整程式碼可以在這裡找到