Go言語でDocBase APIのJSONをシュッと構造体にする #golang

Go言語アドベントカレンダー2の5日目です。

社内で[DocBase](https://docbase.io/)というマークダウンで投稿できるドキュメントの共有ツールを使っているのですが、そのAPIを叩いて、投稿をファイルとしてローカルに保存したい！という話になりました。

そこで同僚と一緒にツールを作っています。([絶賛作りかけ](https://github.com/mom0tomo/blog-post-salvager/))

今回はJSONを取得してGoの構造体に落とし込むまでの流れをご紹介します。
また、いくつか詰まったことがあったので、解決策も挙げていきます。

## APIの実例

[公式ドキュメント](https://help.docbase.io/posts/45703)を参考にしました。

curlだと下記のようにして特定の記事を取得できます。

```shell
curl \
  -H 'X-DocBaseToken: {トークン}’ \
  https://api.docbase.io/teams/{team名}/posts/{投稿ID}
```
取得できるJSONのサンプルはこんな感じです。

```json
 {
  "id": 1,
  "title": "メモのタイトル",
  "body": "メモの本文",
  "draft": false,
  "url": "https://kray.docbase.io/posts/1",
  "created_at": "2015-03-10T12:01:54+09:00",
  "tags": [
    { "name": "rails" },
    { "name": "ruby" }
  ],
  "scope": "group",
  "groups": [
    { "id": 1, "name": "DocBase" }
  ],
  "user": {
    "id": 1,
    "name": "danny",
    "profile_image_url": "https://image.docbase.io/uploads/aaa.gif"
  },
  "comments": []
}
```

## APIサーバーとhttp通信する
net/httpとhttputilを使います。

```go
import (
	"fmt"
	"log"
	"net/http"
	"net/http/httputil"
	"os"
)

func newRequest() (*http.Request, error) {
	url := "https://api.docbase.io/teams/" + os.Getenv("TEAM_DOMAIN") + "/posts/" + os.Getenv("POST_ID")

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}

	req.Header.Set("X-DocBaseToken", os.Getenv("ACCESS_TOKEN"))
	req.Header.Set("Content-Type", "application/json")

	// リクエストヘッダを確認する
	dump, err := httputil.DumpRequestOut(req, true)
	fmt.Printf("%s", dump)
	if err != nil {
		log.Fatal("Error requesting dump")
	}

	return req, err
}

func getResponse() (*http.Response, error) {
	req, err := newRequest()

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	} else if resp.StatusCode != 200 {
		return nil, fmt.Errorf("Unable to get this url : http status %d", resp.StatusCode)
	}

	return resp, err
}
```

DocBaseのAPIを使う際はtokenをヘッダに付与する必要があります。

下記のようにヘッダの内容をdumpとして取得/表示すると、意図した通りに付与されたか確認する際に便利です。

```go
dump, err := httputil.DumpRequestOut(req, true)
```

うまく通信できると、こんな感じでヘッダを表示できます。

```shell
GET /teams/{team名}/posts/{投稿ID} HTTP/1.1
Host: api.docbase.io
User-Agent: Go-http-client/1.1
Content-Type: application/json
X-Docbasetoken: {トークン}
Accept-Encoding: gzip
```

## JSONに対応する構造体をつくる

[Json to Go](https://mholt.github.io/json-to-go/)を使います。サンプルのJSONをcopy & pasteすると対応する構造体を作ってくれます。

API公式のサンプルを貼り付けてみたところ、下記のようなエラーが出ました。 

```shell
Unexpected token c in JSON at position 421
```

APIサンプルの一部が間違っていたのが原因でした。公式だって間違うことはあります。報告して直してもらいましょう。

Json to Goは上のようにわかりやすくエラーを出してくれるので、確認するのが簡単です。

こんな感じの構造体をつくってくれます。

```go
 type Article struct {
	ID        int       `json:"id"`
	Title     string    `json:"title"`
	Body      string    `json:"body"`
	Draft     bool      `json:"draft"`
	URL       string    `json:"url"`
	CreatedAt time.Time `json:"created_at"`
	Tags      []struct {
		Name string `json:"name"`
	} `json:"tags"`
	Scope  string `json:"scope"`
	Groups []struct {
		ID   int    `json:"id"`
		Name string `json:"name"`
	} `json:"groups"`
	User struct {
		ID              int    `json:"id"`
		Name            string `json:"name"`
		ProfileImageURL string `json:"profile_image_url"`
	} `json:"user"`
	Comments []interface{} `json:"comments"`
} 
```


## JSONを構造体に落とし込む
io/ioutillとencoding/jsonを使いました。

```go
import (
	"encoding/json"
	"io/ioutil"
	"log"
)

var articles Article

func encodeJSON() (Article, error) {
	resp, err := getResponse()
	if err != nil {
		return nil, err
	}

	byteArray, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if err := json.Unmarshal(byteArray, &articles); err != nil {
		log.Fatalf("Error!: %v", err)
	}
	return articles, err
}
```

うまく落とし込めない場合、下記のような形でアンマーシャルできないというエラーが出ます。

```shell
2009/11/10 23:00:00 unmrshal failed: json: cannot unmarshal object into Go value of type int
```

私の場合、API公式サンプルを少し拡張して特定の記事一覧を取得しようとしたところこのエラーが出ました。

一旦構造体の怪しいフィールドをinterfaceにして、取得できるか確認してみます。
interfaceの柔軟な形式では取得できる場合、JSONの構造が予想したものと異なる可能性があるので、もう一度curlなどで取得して確認します。

## 構造体から必要なフィールドを取り出す
今回はすべてのフィールドは不要なので、一旦ユーザー名だけ取り出してみます。

こんな感じで構造体がネストしており、値を取り出すときにちょっとだけ詰まりました。

```go
type Article struct {

...


	User struct {
		ID              int    `json:"id"`
		Name            string `json:"name"`
		ProfileImageURL string `json:"profile_image_url"`
	} `json:"user"`


...

} 
```

この場合、下記のようにrangeで回しながらアクセスしていけばOKでした。

```go
	for _, user := range articles.User {
		    id = user.ID
		    name  = user.Name
	    } 
```

  これにてユーザー名だけを取り出すことができました！

```shell
GET /teams/{team名}/posts/{投稿ID} HTTP/1.1
Host: api.docbase.io
User-Agent: Go-http-client/1.1
Content-Type: application/json
X-Docbasetoken: {token}
Accept-Encoding: gzip

mom0tomo
```

## Advent calenderは続く
明日は6日目の@pospomeさんです！