LayerX インボイス を開発しているDX事業部の @yyoshiki41(中川佳希)です。
今回は、json パッケージにある Marshaler
, Unmarshaler
インターフェイスを満たす構造体を用いたアプリケーション実装の例を紹介します。
Marshaler
, Unmarshaler
インターフェイス
Go ではインターフェイスを命名する際、実装されたメソッド名や構造体名の末尾に er
を付ける慣習があります。
Marshaler
, Unmarshaler
も下記のようなメソッドを実装するインターフェイスとして定義されています。
type Marshaler interface { MarshalJSON() ([]byte, error) }
type Unmarshaler interface { UnmarshalJSON([]byte) error }
UnmarshalJSON
は、インプットの json データをハンドルして Go の型へ変換したい時など、
MarshalJSON
は、その構造体を json データとしてシリアライズして出力する際などに利用できます。
例. 文字列 "2021-08-31" を time.Time
型へ。json 出力時には "2021-Aug-31" へ。
package main import ( "encoding/json" "fmt" "log" "time" ) type T struct { Time time.Time `json:"time"` } func (t *T) UnmarshalJSON(b []byte) error { var s string if err := json.Unmarshal(b, &s); err != nil { return err } v, err := time.Parse("2006-01-02", s) if err != nil { return err } t.Time = v return nil } func (t T) MarshalJSON() ([]byte, error) { return json.Marshal(t.Time.Format("2006-Jan-02")) } func main() { b := []byte(`["2021-08-31"]`) t := []T{} err := json.Unmarshal(b, &t) if err != nil { log.Fatal(err) } for _, v := range t { fmt.Printf("struct: %q\n", v) b, err := json.Marshal(v) if err != nil { log.Fatal(err) } fmt.Printf("json: %s\n", b) } }
struct: {"2021-08-31 00:00:00 +0000 UTC"} json: "2021-Aug-31"
アプリケーションでの実装例
店舗側画面での権限を管理するアプリケーションを想定してみましょう。 各ユーザー(店舗のスタッフ)に対して、画面ページ(やデータソース)毎に権限を与えるものです。
UI イメージ
UIクライアント側では、真偽値としてデータを解釈することが先に決まります。
サーバーサイドAPI、DB側ではどのようなデータの持ち方をするでしょうか? 今後もUIが必要とするAPIやデータリソースは変化するという前提とします。
DB
UI で必要となるデータ構造は上でも述べたように真偽値です。
クライアントからのリクエストを保存する際は、UI側と同じ json データでDBに保存しています。(UI側のフォームの部品などが変わった場合にはバージョニングなどで対応が必要です。)
{ "shop": { "view": true, "update": false }, "product": { "view": true, "create": false, "update": false, "delete": false } }
サーバーサイドAPI
アプリケーションコード側ではどのようにこのデータを扱うか? 真偽値によって、アクションを制御できるよう検討してみます。
例えばショップ情報の閲覧権限(view
)が、
true
であれば、GET /shops
という RestAPI へのリクエストが可能false
であれば、リクエストに対してステータスコード403 Forbidden
を返す
という具合です。
今回は、json パッケージのカスタム Marshaler
/Unmarshaler
で解決する方法を考えてみたいと思います。
許可されているAPIエンドポイントを表す構造体をスライスで持つ PermissionShop
構造体を実装します。
(この構造体は、 Marshaler
/Unmarshaler
インターフェイスを満たしています。)
type Path string type Method string type API struct { Path Path Method Method } type PermissionShop struct { View []API `json:"view"` Update []API `json:"update"` } func (p *PermissionShop) UnmarshalJSON(b []byte) error { s := map[string]bool{} if err := json.Unmarshal(b, &s); err != nil { return err } for k, flag := range s { switch k { case "view": if flag { p.View = []API{ {"/shops", "GET"}, } } case "update": if flag { p.Update = []API{ {"/shops", "GET"}, {"/shops", "PUT"}, } } } } return nil } func (p PermissionShop) MarshalJSON() ([]byte, error) { s := map[string]bool{ "view": bool(len(p.View) > 0), "update": bool(len(p.Update) > 0), } return json.Marshal(s) }
真偽値を用いてアクセスコントロールを行うミドルウェアなどでは、DB から取得してきた json を Unmarshal
して、API エンドポイント構造体のスライスにしてハンドル出来るようになります。(真偽値で false
の場合には、スライスの要素数が0として表現されます。)
// DB に保存されていた json データ b := []byte(`{"view": true, "update": false}`) // Goの構造体へ変換を行い、ミドルウェアなどで使用する p := PermissionShop{} err := json.Unmarshal(b, &p) // PermissionShop が、リクエストされたAPIエンドポイントをスライスに含んでいる場合、リクエストが許可される allowed := p.Contains(API)
まとめ
Marshaler
/Unmarshaler
を実装する構造体の実装例を作成していきました。
raw そのままでは扱いにくい json データも、アプリケーションコードで扱いやすい構造体に変換することが出来ます。
もしくは、構造体から json にシリアライズするなどが出来ると必要なコードをスッキリさせたり、ロジックの変化にも対応しやすくなります。
LayerX インボイスでは、 go generate と組み合わせてアクセスコントロールなどに使っています。
- API エンドポイント一覧は、swagger 定義から生成
PermissionResource
は、 UI コンポーネントのフォームから生成UnmarshalJSON
,MarshalJSON
メソッドの実装は、コード生成と相性の良い汎化できる分岐ロジック
開発スピードに耐えれる、変更に強い設計を今後も追求できればと考えています。
興味が湧いたという方は、ぜひ一度話を聞きに来てみてください。
エントリーはちょっとという方、こちらから中の話を聞くこともできます!