Posted at
ClassiDay 3

アプリエンジニアからサーバサイドエンジニアへの JSON オブジェクトに関するお願い

More than 1 year has passed since last update.

Classi Advent Calendar 2016 3 日目です!

API レスポンスの JSON オブジェクトが統一されいないとクライアントサイドでのハンドリングが煩雑になるため、サーバサイド API を作る時に JSON のキー名は適切につけて欲しい、という内容です。

ここでは、クライアントサイドは Swift で書いていきます。


JSON マッピング

例えば users/1 という API が以下のレスポンスを返すとします。

{

"user": {
"id": 1,
"name": "渡辺曜",
"age": 16
}
}

このレスポンスをハンドリングするために、 JSON マッピングをしていくことになります。

ここでは Himotoki を使っています。

struct User: Decodable {

let id: Int
let name: String
let age: Int

static func decode(_ e: Extractor) throws -> User {
return try User(
id: e <| "id",
name: e <| "name",
age: e <| "age"
)
}
}

これで、JSON を Swift で扱えるようになりました。

user.id, user.name などでアクセスできるようになり、型も担保されいい感じです。 :blush:


別の API の user オブジェクト

users/1/best_friend という API のレスポンスは以下のようになっていました。

{

"user": {
"id": 2,
"first_name": "千歌",
"last_name": "高海"
}
}

なんということでしょう、 user というキーは同じ名前なのに first_name と last_name という新しいものがあり、 age と name は消えてしまいました。 :scream:

このレスポンスをマッピングするとこうなります。

struct User: Decodable {

let id: Int
let firstName: String
let lastName: String

static func decode(_ e: Extractor) throws -> User {
return try User(
id: e <| "id",
firstName: e <| "first_name",
lastName: e <| "last_name"
)
}
}

しかし困ったことに User struct が 2 つ生まれてしまい、名前が衝突してしまいました... :cry:


クライアント側で解決するには?

struct 名を UserOfFriend など変えるか、 optional を使うしかありません。


struct 例:

struct UserOfFriend: Decodable { ...

名前は衝突しなくなりました。

しかし、 JSON のキー名とオブジェクトの属性名が違ってしまいます。 :angry:


optional 例:

struct User: Decodable {

let id: Int
let firstName: String?
let lastName: String?
let name: String?
let age: Int?

static func decode(_ e: Extractor) throws -> User {
return try User(
id: e <| "id",
firstName: e <| "first_name",
lastName: e <| "last_name",
name: e <| "name",
age: e <| "age"
)
}
}

こうすれば users/1 でも users/1/best_friend でも同じ User として利用できます。

ですが、これだと毎回 nil かどうかの確認をしなければならず、よくありません。

面倒くさくて nil チェックを怠る野蛮な人間が今後出てくる可能性もあります。 :cop:


どうしてほしいか

クライアントで頑張には限界があるため、オブジェクトの中身が違うなら名前を変えてレスポンスを返して欲しいです。

userfriend のように JSON キー名を変えてもらうだけで、 UserFriend と別の型を用意することができました! :tada:

api
json
mapping

users/1
{
  "user": {
    "id": 1,
    "name": "渡辺曜",
    "age": 16
  }
}

struct User: Decodable {
    let id: Int
    let name: String
    let age: Int

    static func decode(_ e: Extractor) throws -> User {
        return try User(
            id: e <| "id",
            name: e <| "name",
            age: e <| "age"
        )
    }
}

users/1/best_friend
{
  "friend": {
    "user_id": 2,
    "first_name": "千歌",
    "last_name": "高海"
  }
}
struct Friend: Decodable {
    let id: Int
    let firstName: String
    let lastName: String

    static func decode(_ e: Extractor) throws -> Friend {
        return try Friend(
            id: e <| "id",
            lastName: e <| "last_name",
            firstName: e <| "first_name"
        )
    }
}


最後に

JSON オブジェクトの命名を適切にしていただくだけで、クライアント側の負荷が減ります :smiley:

私も API を作っていると同じ名前で別のオブジェクトを生成してしまうことがあるので、そうはならないように気をつけたり、 JSON を生成する処理をキーごとに共通化しています。

Rails でいうと、 jbuilderpartial を細かく分割するなどで対応出来ると思います。

(ただし jbuilderpartial は重いのであまり使うなという話も...)

配列なら users のように複数形にしてほしい、必須パラメータなら URLParameter でなく path に含めてほしいなどいろいろあります。

サーバサイドでやってほしいこと、クライアントサイドでやるべきことの認識を合わせ、互いに定時退社できるよう協力していきましょう!!