LoginSignup
3
2

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-04

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さんです!

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2