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 などでアクセスできるようになり、型も担保されいい感じです。 ![]()
別の API の user オブジェクト
users/1/best_friend という API のレスポンスは以下のようになっていました。
{
"user": {
"id": 2,
"first_name": "千歌",
"last_name": "高海"
}
}
なんということでしょう、 user というキーは同じ名前なのに first_name と last_name という新しいものがあり、 age と name は消えてしまいました。 ![]()
このレスポンスをマッピングするとこうなります。
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 つ生まれてしまい、名前が衝突してしまいました... ![]()
クライアント側で解決するには?
struct 名を UserOfFriend など変えるか、 optional を使うしかありません。
struct 例:
struct UserOfFriend: Decodable { ...
名前は衝突しなくなりました。
しかし、 JSON のキー名とオブジェクトの属性名が違ってしまいます。 ![]()
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 チェックを怠る野蛮な人間が今後出てくる可能性もあります。 ![]()
どうしてほしいか
クライアントで頑張には限界があるため、オブジェクトの中身が違うなら名前を変えてレスポンスを返して欲しいです。
user と friend のように JSON キー名を変えてもらうだけで、 User と Friend と別の型を用意することができました! ![]()
| 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 オブジェクトの命名を適切にしていただくだけで、クライアント側の負荷が減ります ![]()
私も API を作っていると同じ名前で別のオブジェクトを生成してしまうことがあるので、そうはならないように気をつけたり、 JSON を生成する処理をキーごとに共通化しています。
Rails でいうと、 jbuilder の partial を細かく分割するなどで対応出来ると思います。
(ただし jbuilder の partial は重いのであまり使うなという話も...)
配列なら users のように複数形にしてほしい、必須パラメータなら URLParameter でなく path に含めてほしいなどいろいろあります。
サーバサイドでやってほしいこと、クライアントサイドでやるべきことの認識を合わせ、互いに定時退社できるよう協力していきましょう!!