LayerX エンジニアブログ

LayerX の エンジニアブログです。

Go: json パッケージ Marshaler/Unmarshaler の実装例

LayerX インボイス を開発しているDX事業部の @yyoshiki41(中川佳希)です。

今回は、json パッケージにある Marshaler, Unmarshaler インターフェイスを満たす構造体を用いたアプリケーション実装の例を紹介します。

Marshaler, Unmarshaler インターフェイス

Go ではインターフェイスを命名する際、実装されたメソッド名や構造体名の末尾に er を付ける慣習があります。 Marshaler, Unmarshaler も下記のようなメソッドを実装するインターフェイスとして定義されています。

type Marshaler interface {
    MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
    UnmarshalJSON([]byte) error
}

pkg.go.dev

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 イメージ

f:id:yyoshiki41-lx:20210830104135p:plain

UIクライアント側では、真偽値としてデータを解釈することが先に決まります。

サーバーサイドAPI、DB側ではどのようなデータの持ち方をするでしょうか? 今後もUIが必要とするAPIやデータリソースは変化するという前提とします。

DB

UI で必要となるデータ構造は上でも述べたように真偽値です。

f:id:yyoshiki41-lx:20210830135024p:plain

クライアントからのリクエストを保存する際は、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 を返す

という具合です。

f:id:yyoshiki41-lx:20210830135932p:plain

今回は、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 メソッドの実装は、コード生成と相性の良い汎化できる分岐ロジック

開発スピードに耐えれる、変更に強い設計を今後も追求できればと考えています。

興味が湧いたという方は、ぜひ一度話を聞きに来てみてください。

herp.careers

エントリーはちょっとという方、こちらから中の話を聞くこともできます!

meety.net