json作る時の自分なりの考え方


はじめに

最近、Laravelのようなフレームワークを使って、controllerからbladeに変数を渡したりして作るフロントエンドやバックエンドが合わさった開発だけでなく、バックエンド、フロントエンドを分けてAPIで通信することでバックにgoを選択して、フロントにnextjsを使ったりそれらを入れ替えたりみたいなことがしやすくなってきた。またAPIを使うことで外部サービスとの連携もしやすくなってきてjsonを使う機会が増えてきてると思う。
仕事でもjsonの定義などをしたりするようになり、自分的にどういったjsonを定義することで使いやすいのか結構悩んでる。(今もまだ悩んでる)
しかし、私はjsonを作る際にその構造を選んだ判断基準とかが存在すると思うので、それが何かを知りたい!
そしてjsonを書く際の基本的な考え方があると思っているのでそこも知りたい!ってことで、今の自分の考え方がベストプラクティスではないと思うけど当面はこの考え方でやろうと思えたのでその考え方をまとめてみました。🙋‍♂️

ここで話さないこと

  • jsonとは何かみたいな基本なところ
  • 初めから制約付ければ良いやんみたいなところ
    考えるのめんどくさい時は制約を加えるとフォーマットを強制できるので良いですが、自分でもある程度そういった基本的な書き方ができた方が良いので自力で頑張る!!を選択します。制約に関してはここら辺を参考にしてもらえれば。

https://qiita.com/tkawa/items/2841e155e5b51c09ed40

tl;dl

jsonはデータを表すフォーマット。そのため大事なのはデータやデータ構造をちゃんと表現できるのかだと思う。例えば、構造的にデータが同じ塊であるのに分けて返したりすると取得する側でめんどくさい。あくまで[キー:値]というのを忘れない。そのため、そのデータには何が紐づいてくるか、紐づいていないのかも考えて構造を決めることが大切だと思う。
また、[キー:値]がこのレスポンスにはあって、このレスポンスにはないみたいな返し方をして必要なデータ構造の情報が入ってたり入ってなかったりする状態はない方がいい気がする。構造的にキーや値が存在してるならにレスポンスにも存在させて、値がないなら値はないという情報を付与した方がいい。


どうやってjson作っていけばいい?

Apiの定義を決める時にjsonの構造に関していつも悩む。jsonは定義次第でどんな形でも送れる。けどそれ故にどういう構造が送る側にも、受け取る側にもいいのかいまいちわからない。実際に送る側、受け取る側を作ることで理解が深まるのだろうけどそこまで構造を考えるほどのものを一から作るのめんどくさい😵
ということでこれはあくまで自分の頭の中で考えたやつなので実際取りやすいのか取りにくいのかわからん。けど分かりやすい構造は、分かりやすく取れると信じている😤(謎の自信)

その中で、json構造を決める際の基本的な考え方や判断基準をある程度明確にすることで書きやすくなるのではないか?と思います。

自分的に大切だと思うのは、

  1. キーに対しての値をはっきりさせる
  2. データが複数あるのであれば、~1とか数字をつけるのではなく連想配列に
  3. 関連したデータはできる限りまとめ、関連していないものは分ける

だと思っていて、それぞれ持論を展開していこうと思います。

1.キーに対しての値をはっきりさせる

まず、jsonの書き方として[キー:値]という書き方がありますが、仕事でswaggerをみていると結構キーという部分が蔑ろになっているのではないのかなと思います。キーの部分が適当だとその値が何を表しているのかが分かりづらくなるのでこれは大切です。[果物:りんご]が[人間:りんご]だと何いってるかわからないのでキーはその値を表すものは何かという部分を表す必要があります。分かりやすさは正義!

例えばblogの記事を一つ返すみたいなのデータがあったとします。

{
  "data" : {
    "title": "title",
    "coment": "coment"
  }
}

この場合闇雲にdataというキーを使うべきではないと思います。dataという一見良さそうなものであってもこのキーが何を表しているのかわかりづらい。もしdataをキーとして使うのであればデータの集まりを表現するみたいな形で使って配列で書くほうが分かりやすいです。(次の部分で説明します)
なのでこの場合データが何を表しているのかをキーで定義します。

{
  "blog": {
    "title": "title",
    "content": "content"
  }
}

感覚としてはblogデータを構成するための値(metaデータみたいな雰囲気)としてフィールドにtitle, contentを定義する感じ。
ただ、
https://server/blog/1
の様にRESTで書いてるとかだとurlに何を返すかみたいなのが想像できるので、この場合はキーがdataでもいいかもしれない。

しかしユーザー情報や、他の情報を含めて返す場合を考えるとdataに入れてると不自然な感じになる。

{
  "data": {
    "title": "title",
    "conetnt": "content",
    "user_id": user_id,
    "user_name": "user_name"
  }
}

こうなってくると、blogの情報だけが欲しいのにuser情報まで入ってくるみたいな状態になっていまう。そこまで問題になるってことはない気もするけど。使い方次第な気がs。
他の問題点としては同じ様な名前が出た場合競合してしまう、そしてどのキーに紐づいている値なのかが分かりづらくなる可能性がある。
仮にユーザーが受けたブログの総いいね数、ブログ記事のいいね数のカウントが入ってくることになったとして、

{
  "data": {
    "title": "title",
    "conetnt": "content",
    "user_id": user_id,
    "user_name": "user_name",
    "user_likes_count": 30, // 競合を回避するためにuserを付与する
    "likes_count": 10
  }
}

こうなってくるとcommentを今後追加することになってコメントのいいねとか作るってなってくるとcomment_likes_countとかなりそうで冗長なのでやめたい。実際どういうdb構成にするかは知らんからこんな感じで返すことはないかもしれんけど要はテーブル分けるみたいにjsonでも分けた方がいいよねーって思う。😇

そのため関係性が違うデータを分けることで上記部分を回避する。

{
  "blog": {
    "title": "title",
    "content": "content",
    "likes_count": 20,
  },
  "user": {
    "user_id": user_id,
    "user_name": "user_name",
    "likes_count": 30 // 総いいねなのかわかりづらい項目ではある
  }
}

これでキーに対する値が何を表しているのかがある程度明確になる。↑のjsonだと総いいねなのかはちょっと分かりづらいけど、少なからず名前の競合はないのでlikes_countとして定義できる。またuser_idとしなくてもidで表現できるので、今後commentを含めることになってもcomment_idとせずにidと定義できる。

2.データが複数あるのであれば、~1とか数字をつけるのではなく連想配列に

次にデータが複数の場合の表現です。たまに~1, ~2みたいなキーを設定されてる時があるのですが使いにくいからやめてほしいって心底思います。(たまにdbでもそういうのされてるときある)
そうやって複数を表現すると後で取得したりする時にしんどいので、複数なら配列という良いやつがいるので配列を使って定義してあげることが大切だと思います。dataは分かりづらいと言いましたがデータの集まりとしてのdataとして定義するのはいいと思います。(これでもdataがなんの配列を定義しているかがある程度わかる前提が必要)
例えば、
https://server/user/1/blogs

{
  "data" : [
    {
      "title": "title1",
      "content": "content1"
    },
    {
      "title": "title2",
      "content": "content2"
    },
  ]}
}

この場合urlなどをみた時にdataでもある程度わからなくはないので複数の記事が紐づいているのかな?と想像できます。
ただ、あくまでblogsのデータが返ってくると想像できる時にdataとすべきだと思います。やはり1でも書きましたが、ユーザーの情報が入ってきたり複数のデータが入ってくる場合はdataではなくblogsみたいなキーにして分けるべきだと思います。

3.関連したデータはできる限りまとめ、関連していないものは分ける

診断アプリみたいなのがあり質問に対しての答えによって次の質問へ分岐して、また質問みたいな流れがあったとします。↓

             q1 好きな国は??

    A1            A2             A3
イギリスの画像     日本の画像   アメリカの画像
a1 イギリス       a2 日本       a3 アメリカ
   説明           説明           説明

みたいな感じのを想像してもらえると。
内部には次の質問へ分岐する情報をa1, a2, a3が持っているとします。また画像や説明もそれらa1などが持っていると仮定します。

jsonにしていきます

まず自分的によくないと思うjsonから
このjson構造的にはquestion_1のなかにa1, a2, a3というキーがありますが、それぞれanswer, image, description,そしてnext_questionsの値として複数存在していて繋がり(紐付き)を持っています。

{
  "data": {
    "question_1": { // 複数くるとするとquestion_2とか2とかになりそう😨
      "title": "好きな国は",
      // 答え
      "answers": {
        "a1": "イギリス",
	"a2": "日本",
	"a3": "アメリカ"
      },
      // 画像
      "images": {
        "a1": "url1",
	"a2": "url2",
	"a3": "url3"
      },
      // 説明
      "descriptions": {
        "a1": null,
	"a2": null,
	"a3": "国の説明"
      },
      // 次の質問
      "next_questions": {
        "a1": "question_2", // Q2にいく
	"a2": "question_3", // Q3にいくみたいな分岐
	"a3": null
      }
    }
  }
}

この場合の問題点はいくつかありそうですが、まずdataキーに対しての値がオブジェクトになっているのに[question_1]が定義されていて、なんか複数存在しそうな気配がしていることです。(2番目で説明した配列になってないってやつです)
この場合question_2とか増えると取得する時に、こんな感じでquestion_のところを一回全部取ってきてそれをforeachで回していけない気がするしバグが多そうです。

`question_ + ${i}`

次にanswers, images, descriptions, next_questionsは共にa1のデータ、a2のデータとして紐付きいているはずなのにそれらの紐付き(まとまりでも良さそう)がないため、いちいち条件分岐などを使って気合で引っ張って来ないといけない。1の「キーに対しての値をはっきりさせる」に関してはできていそうですが、a4, a5...と回答が増えると冗長になってきそうです。
また、紐付きがわかりづらいと答え(answer)に紐づくimages, descriptionsとして定義しているつもりでも使う側としてはなんのimagesなのかよく分かりません。キーがa1になってるので完全にわからないということはないですが、答えがそいつらの情報を持っているんだよ!という情報を伝えれた方が綺麗な気がします。

紐付きってなんなん?

って言われると言葉で説明するの難しいのですが、自分の中では同じフィールドに上に存在するのキーを表現するmetaデータとして[キー:値]を定義する感じだと思っているので、この場合のimagesやdescriptionsは上のquestion_1を構成するキーとして定義されているみたいな印象を受けます。前提としてはa1, a2などがimage, descriptionに紐づく想定。そのため、それら紐付きが感じられるデータは同じ階層にまとめることである程度分かりやすくなります。

自分なりに直してみると、

{
  "data": [
    // まず複数返すなら配列で
    // そしてその中でquestionを一塊にしていく様にキーを決める。
    {
      "question": 1, // id
      "title": "好きな国は?",
      // a1, a2の中に、[答え]の値として紐づいているimageやdescription,next_questionなどを入れる。
      // a1, a2と複数あるのでそれをanswersキーの値として配列で持つ。
      "answers": [
        {
	  "answer": "イギリス",
	  "image": "url1",
	  "description": null,
	  "next_question": 2
	},
	{
	  "answer": "日本",
	  "image": "url2",
	  "description": null,
	  "next_question": 3
	},
	{
	  "answer": "アメリカ",
	  "image": "url3",
	  "description": "国の説明",
	  "next_question": null
	}
       ]
     }
   ]
}

自分だとこんな感じになるのかなと思います。(フロント側でどう扱うか等で書き方は変わるかも)

複数のa1, a2というキーでimagesやdescriptionsを持っていましたが、 answersの値として連想配列を作成しa1という値(a1がオブジェクトになった感じ)の中にそれを含めることでいちいちimagesから取ってきてdescrptionsからa1のデータを取ってきてとやらずともa1オブジェクトの中にそれらがまとまっています。またa4, a5...となってもanswersは連想配列として定義しているので増えても問題はないですし、何よりa1みたいなキーを作って各データに紐づける必要がないので綺麗です。

コードだとイメージこんな感じ

いちいちimagesやdescriptionから取ってくる方

const image = image[a1]
const description = description[a1]

まとめた方

const a = answers[0] // a1にまとまっているので複数の情報のアクセスがしやすい
a.image
a.description

これらの書き方を意識することでdataは複数のquestionが入ってくることを想像できる、かつanswersのなかに必要な画像、説明、次の質問など必要なデータもまとまっているので答えの中にはこれらの情報が付与されていると推測しやすくなると思います。(他いい書き方あったら教えて欲しい。)

また気を付けるべきというか自分的にあまりやられると嬉しくないのが、こんな感じで連想配列にしてくれたのに、nullならキーごと消して送られてくることです。

...
      "answers": [
        {
	  "answer": "イギリス",
	  "image": "url1",
	  // "description": null, //がついているのは送られてこないと思ってください。
	  "next_question": 2
	},
	{
	  "answer": "日本",
	  "image": "url2",
	  // "description": null,
	  "next_question": 3
	},
	{
	  "answer": "アメリカ",
	  "image": "url3",
	  "description": "国の説明",
	  // "next_question": null
	}
       ]
...

//の部分が送られてこない様にすると取得する側ではdescriptionがなかった場合を考慮しなくてはいけません。内部で使うことを想定しているならここはこない場合があるよーみたいに伝えられるので良いですが、外部サービスのAPIでそれをやられると他のキーもない場合があるの?みたいにわからなくなるのでないなら送らないでは、なくないなら値はないという情報を送ると親切かなと思います。

終わりに

これらを意識することである程度おんなじような書き方になるのではないのかなと思います。まぁ今は簡単な例でしか考えてないから同じ書き方になるのかなと感じているだけで実際はプロジェクトとかによってもうちょい複雑になるかもしれませんが。
ただ、考えてみるとなんかdbの設計に似たものを感じます。テーブルを作成する時にどのデータが必要になりそうなのか、そのテーブルのデータ必要なものをまとめて、違うやつは違うテーブルで分けるみたいな書き方や考え方に近くなるのかなと思います。知らんけど🤪
正直まだ自分が仕事に使う時に当てはめて検証してるって段階なのでこれが正解ではないってことはわかってもらえると助かります。ただこれを見てよりいい方法が思いついたとかになってくれると嬉しいです。あと教えてください。🙇‍♂️