構造体のフィールドをJSON Schemaでvalidationする

# はじめに
構造体のstringフィールドにJSONを格納するときにJSON Schemaで手軽にvalidateする方法を紹介します。

# gopkg.in/go-playground/validator.v9
validatorには[gopkg.in/go-playground/validator.v9](https://gopkg.in/go-playground/validator.v9)を使います。

構造体の各フィールドに正しい値が入っているかどうかをお手軽にチェックするライブラリです。型だけではチェックできないような性質もチェックできるようになります。
他にも似たようなライブラリはありますがこれが一番validationの種類が多くシンプルでした。

構造体に`validate`タグでvalidationの種類を指定するだけでチェックできるようになります。
例えば次のような構造体`User`を定義できます。

```go
type User struct {
  Username string `validate:"required,printascii,max=16,"`
  Name     string
  Email    string `validate:"required,email"`
  Age      int    `validate:"min=0"`
}
```

これ以外にも豊富なvalidationが用意されています。
https://godoc.org/gopkg.in/go-playground/validator.v9

また、後述しますが自分でカスタムvalidationを定義することもできます。

validationを定義したら次のようにして構造体をチェックします。。

```go
package main

import (
        "fmt"

        "gopkg.in/go-playground/validator.v9"
)

type User struct {
        Username string `validate:"required,printascii,max=16"`
        Name     string
        Email    string `validate:"required,email"`
        Age      int    `validate:"min=0"`
}

func main() {
        v := validator.New()

        fmt.Println("---- valid values")
        u := User{
                Username: "valid user",
                Email:    "valid@example.com",
                Age:      20,
        }
        if err := v.Struct(u); err == nil {
                fmt.Println("Valid!")
        } else {
                fmt.Println("Invalid!", err)
        }

        fmt.Println("---- invalid values")
        u = User{
                Username: "invalid user",
                Email:    "invalid",
                Age:      20,
        }
        if err := v.Struct(u); err == nil {
                fmt.Println("Valid!")
        } else {
                fmt.Println("Invalid!", err)
        }
}
```

実行結果は次のようになります。

```
---- valid values
Valid!
---- invalid values
Invalid! Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag
```

# github.com/xeipuuv/gojsonschema
JSON Schemaには[github.com/xeipuuv/gojsonschema](https://github.com/xeipuuv/gojsonschema)を使います。

次のようにしてJSONをチェックできます。

```go
package main

import (
        "fmt"

        "github.com/xeipuuv/gojsonschema"
)

func main() {
        schemaLoader := gojsonschema.NewStringLoader(`{"type":"string"}`)

        fmt.Println("---- Valid JSON")
        jsonLoader := gojsonschema.NewStringLoader(`"string"`)
        result, _ := gojsonschema.Validate(schemaLoader, jsonLoader)
        if result.Valid() {
                fmt.Println("Valid!")
        } else {
                fmt.Println("Invalid!")
        }

        fmt.Println("---- Invalid JSON")
        jsonLoader = gojsonschema.NewStringLoader(`{}`)
        result, _ = gojsonschema.Validate(schemaLoader, jsonLoader)
        if result.Valid() {
                fmt.Println("Valid!")
        } else {
                fmt.Println("Invalid!")
        }
}
```

実行結果は次のようになります。

```
---- Valid JSON
Valid!
---- Invalid JSON
Invalid!
```

このサンプルでは文字列でJSON SchemaやJSONを与えていますが、ファイルから直接読み込むことも可能です。

# 組み合わせる
この2つを組み合わせて次のようにして構造体のフィールドのJSONをJSON Schemaでチェックすることを目指します。

```go
type User struct {
        Username string `validate:"required,printascii,max=16"`
        Profile  string `validate:"required,jsonschema=profile"`
}
```

gopkg.in/go-playground/validator.v9には独自のカスタムvalidationを追加する機能があり、そのカスタムvalidationでJSON Schemaによるチェックを実装します。

JSON Schemaによるカスタムvalidationは次のように実装できます。

```go
var schema = map[string]string = {
  // JSON Schema
}

func isValidJSON(fl validator.FieldLevel) bool {
        json := fl.Field().String()
        field := fl.Param()

        schemaLoader := gojsonschema.NewStringLoader(schema[field])
        jsonLoader := gojsonschema.NewStringLoader(json)

        result, _ := gojsonschema.Validate(schemaLoader, jsonLoader)
        return result.Valid()
}
```

`fl.Field().String()`でフィールドの値、つまりJSONがとれます。
また、`fl.Param()`でvalidationのパラメータがとれます。今回の場合は`"profile"`になります。

このサンプルではパラメータによって`schema`というmapからJSON Schemaを取り出すようになっています。

このカスタムvalidationを`validate:"jsonschema=xxx"`で使えるようにするには次のようにします。

```go
v := validator.New()
v.RegisterValidation("jsonschema", isValidJSON)
```

最終的にこんな感じになります。

```go
package main

import (
        "fmt"

        "github.com/go-playground/validator"
        "github.com/xeipuuv/gojsonschema"
)

type User struct {
        Username string `validate:"required,printascii,max=16"`
        Profile  string `validate:"required,jsonschema=profile"`
}

const profileSchema = `
{
        "type":"object",
        "properties":{
                "age": {
                        "type":"integer",
                        "minimum":0
                }
        }
}
`

var schema = map[string]string{"profile": profileSchema}

func main() {
        v := validator.New()
        v.RegisterValidation("jsonschema", isValidJSON)

        fmt.Println("---- valid values")
        u := User{
                Username: "valid user",
                Profile:  `{"age":20}`,
        }
        if err := v.Struct(u); err == nil {
                fmt.Println("Valid!")
        } else {
                fmt.Println("Invalid!", err)
        }

        fmt.Println("---- invalid values")
        u = User{
                Username: "invalid user",
                Profile:  `{"age":-666}`,
        }
        if err := v.Struct(u); err == nil {
                fmt.Println("Valid!")
        } else {
                fmt.Println("Invalid!", err)
        }
}

func isValidJSON(fl validator.FieldLevel) bool {
        json := fl.Field().String()
        field := fl.Param()

        schemaLoader := gojsonschema.NewStringLoader(schema[field])
        jsonLoader := gojsonschema.NewStringLoader(json)

        result, _ := gojsonschema.Validate(schemaLoader, jsonLoader)
        return result.Valid()
}
```

実行結果は次のようになります。

```
---- valid values
Valid!
---- invalid values
Invalid! Key: 'User.Profile' Error:Field validation for 'Profile' failed on the 'jsonschema' tag
```

# おわりに
今回のソースコードはこちらに置いてます。
https://github.com/nownabe/examples/tree/master/jsonschemavalidation

データベースに挿入するときにvalidatorを使うこともあるかと思いますが、この方法を使うことでJSON型のスキーマも同じようにチェックできるようになります。