Go言語アドベントカレンダー2の5日目です。
社内でDocBaseというマークダウンで投稿できるドキュメントの共有ツールを使っているのですが、そのAPIを叩いて、投稿をファイルとしてローカルに保存したい!という話になりました。
そこで同僚と一緒にツールを作っています。(絶賛作りかけ)
今回はJSONを取得してGoの構造体に落とし込むまでの流れをご紹介します。
また、いくつか詰まったことがあったので、解決策も挙げていきます。
APIの実例
公式ドキュメントを参考にしました。
curlだと下記のようにして特定の記事を取得できます。
curl \
-H 'X-DocBaseToken: {トークン}’ \
https://api.docbase.io/teams/{team名}/posts/{投稿ID}
取得できる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を使います。
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として取得/表示すると、意図した通りに付与されたか確認する際に便利です。
dump, err := httputil.DumpRequestOut(req, true)
うまく通信できると、こんな感じでヘッダを表示できます。
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を使います。サンプルのJSONをcopy & pasteすると対応する構造体を作ってくれます。
API公式のサンプルを貼り付けてみたところ、下記のようなエラーが出ました。
Unexpected token c in JSON at position 421
APIサンプルの一部が間違っていたのが原因でした。公式だって間違うことはあります。報告して直してもらいましょう。
Json to 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を使いました。
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
}
うまく落とし込めない場合、下記のような形でアンマーシャルできないというエラーが出ます。
2009/11/10 23:00:00 unmrshal failed: json: cannot unmarshal object into Go value of type int
私の場合、API公式サンプルを少し拡張して特定の記事一覧を取得しようとしたところこのエラーが出ました。
一旦構造体の怪しいフィールドをinterfaceにして、取得できるか確認してみます。
interfaceの柔軟な形式では取得できる場合、JSONの構造が予想したものと異なる可能性があるので、もう一度curlなどで取得して確認します。
構造体から必要なフィールドを取り出す
今回はすべてのフィールドは不要なので、一旦ユーザー名だけ取り出してみます。
こんな感じで構造体がネストしており、値を取り出すときにちょっとだけ詰まりました。
type Article struct {
...
User struct {
ID int `json:"id"`
Name string `json:"name"`
ProfileImageURL string `json:"profile_image_url"`
} `json:"user"`
...
}
この場合、下記のようにrangeで回しながらアクセスしていけばOKでした。
for _, user := range articles.User {
id = user.ID
name = user.Name
}
これにてユーザー名だけを取り出すことができました!
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さんです!