[Swift 4] Codableでスネーク記法のJSONに対応するための一例

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

モバイルアプリサービス部の中安です。

最近、Alexaのことばかり書きすぎて疎かになっているswiftの記事を書きます。

Codable っていいよね

Codable は Swift4 から搭載されたデータ構造化に便利なプロトコルです。

以前に自分も Codable についてはブログ ([Swift 4] SwiftyJSONを使わずにシンプルにJSONをデータ構造化する) にしてまとめておりました。

それにしても非常に強力な機能ですね。

しかし、問題が

そんな自分の拙文なブログ記事を見てくれたという知人が嘆いておりました。

どういうことかというと、例えば下記はとある学校の生徒名簿のJSONデータとします。

[
    {
        "student_name": "佐藤",
        "grade": 3,
        "room_number": 1
    },
    {
        "student_name": "鈴木",
        "grade": 2,
        "room_number": 3
    }
]

「生徒の名前」と「学年」と「教室の番号」が書かれたデータです。この形式のJSONオブジェクトを扱うために Student という構造体を定義するとします。

struct Student: Codable {
    let studentName: String
    let grade: Int
    let roomNumber: Int
}

Student 構造体を Codable に準拠させておくことで、JSONから下記のように簡単にデコードができます。

let jsonString = <先ほどのJSON文字列>
let students = try? JSONDecoder().decode([Student].self, from: jsonString.data(using: .utf8)!)

しかし、残念ながら期待に反して students には nil が入ってしまいます。

JSON側は "student_name" というスネーク記法のキーなのに対して、デコーダは "studentName" というキャメル記法のプロパティ名キーを探しに行くため「見つかりませんよ」というエラーになるわけです。

そりゃあ、そうなので・・・

これを解決するため、調べてもっとも出てくる方法は、Decodable が要求するイニシャライザを実装する方法になるかと思います。

struct Student: Codable {
    let studentName: String
    let grade: Int
    let roomNumber: Int
    
    enum Key: String, CodingKey {
        case studentName = "student_name"
        case grade
        case roomNumber = "room_number"
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Key.self)
        self.studentName = try container.decode(String.self, forKey: .studentName)
        self.grade       = try container.decode(Int.self, forKey: .grade)
        self.roomNumber  = try container.decode(Int.self, forKey: .roomNumber)
    }
}

CodingKey プロトコルに準拠した String の enum にJSONキーを定義しておくことで、キーの記法の揺れを吸収できるようになります。

しかし、知人の話では「これが長くて煩わしい」という話でした。

たしかに、例の上では3つの要素しかないので書くのはそれほど大変ではありませんが、構造体の要素数が増えれば enum の種類も増やさないといけないし、イニシャライザの内容も増やさなくてはならない。エンコードの処理がもし増えたら、さらに・・・という感じです。

また、記法について対応する必要のない grade のキーまでわざわざ対応してやらなくてはいけないところもイヤな感じです。

WebAPI等で返されるJSONを扱うとき、大概はそのJSONキーはスネーク記法であることが多いのではないかと思います。しかし、その対応のために沢山のコードを書くのは避けたいところです。少なくとも CodingKey の定義や init(from:) あたりを削っていく方向にしたいところです。

というわけで、その策をちょっと考えてみました。

策1

「いっそ、プロパティの名前をスネーク記法にしてしまおう!」という強引なやり方です。

struct Student: Codable {
    let student_name: String
    let grade: Int
    let room_number: Int
}

これで実際にJSONのキーとプロパティ名が合致するので、ちゃんと動くことにはなります。

でも、やばり Swift の命名規則としてプロパティ名はキャメル記法なわけですからすごく違和感があります。

策2

「JSONのキーはスネーク記法、構造体のプロパティ名はキャメル記法」という体裁を崩さずに、かつ冗長化しない方法として下のような方法を考えました。

struct Student: Codable {
    let grade: Int
    private let student_name: String
    private let room_number: Int
    
    var studentName: String { return student_name }
    var roomNumber: Int { return room_number }
}

private let としてプロパティを定義しておいてもデコーダはちゃんとキーを見つけて値を入れてくれます。その特性を使う形でスネーク記法になるキーだけ private let で定義しておきます。

あとは、public な形でキャメル記法の計算プロパティを置いておいて、スネーク記法のプロパティ値をただ返すようにします。

すると、この構造体オブジェクトを外から使用する際には student.studentName student.grade student.roomNumber しか使用できなくなり、おおよそやりたかったことに合致します。

ひとつのキーに対して二重で定義するような格好になってしまいますが、シンプルにやるならこの方法なのではないかなぁと思います。

未来の策

さて、そんな中で Swift4.1 に関する話を目にしました。

Swift 4.1 improves Codable with keyDecodingStrategy (Swift4.1でCodableはkeyDecodingStrategyによって改善される)によると、デコーダに簡単な設定をしてやることで、取り込み先のデータがスネーク記法であってもデコード時に取り込んでくれるようになるようです。

執筆時点ではベータなので詳細なことは書けませんが、すでに公式リファレンスにも JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase の記述があるようです。(正式版には変更が加わる可能性があるので、ご注意ください)

もう少し待てばいいことありそうです。

最後に

今日書いたことは、あくまで一例になります。もしかして、もっといい手があるよという方がいれば教えてください。また、ちょっとこの問題に詰まっていたんだという方の参考になれば幸いです。