[Xcode/Swift] 今更だけど、ObjectMapperを学び直す②

前回の記事:

[Xcode/Swift] 今更だけど、ObjectMapperを学び直す①

今回から、基本的な使い方とマッピング手法を学んでいきます。


4. 基本的な使い方

Mappableプロトコルとは

ObjectMapperの中心となるのが Mappable プロトコルです。

protocol Mappable {
    init?(map: Map)
    mutating func mapping(map: Map)
}

この2つを実装することで、JSON → Model / Model → JSONの相互変換が可能となります。

基本的な考え方:

どのJSONキーを、どのプロパティに割り当てるか」を明示的に書く

これを一旦理解しておけばOKです。


init?(map:) の役割

init?(map:) = 初期化のタイミングで呼ばれるイニシャライザ

init?(map: Map) {
}

基本的に中身は空のままで問題ありません。
しかし、以下のような用途で使われることがあります。

  • 必須項目が存在しない場合に nil を返す
  • mapの中身をチェックして生成可否を判断する

例:

init?(map: Map) {
    if map.JSON["id"] == nil {
        return nil
    }
}

// = 「このJSONからModelを生成してよいか?」を判断する場所

mapping(map:) の書き方

実際のマッピング処理は mapping(map:) に書きます。

mutating func mapping(map: Map) {
    id   <- map["id"]
    name <- map["name"]
}

ここで使われている <- は、ObjectMapper独自の演算子です。

  • 左辺:Modelのプロパティ
  • 右辺:JSONのキー

という対応関係を表しています。


JSON → Model の変換例

実際のJSONを例に見てみましょう。

{
  "id": 1,
  "name": "Arthur"
}

対応するModelは以下のようになります。

import ObjectMapper

struct User: Mappable {
    var id: Int?
    var name: String?

    init?(map: Map) {}

    mutating func mapping(map: Map) {
        id   <- map["id"]
        name <- map["name"]
    }
}

変換処理はシンプルです。

let jsonString = """
{
  "id": 1,
  "name": "Arthur"
}
"""

if let user = Mapper<User>().map(JSONString: jsonString) {
    print(user.name) // Optional("Arthur")
}

5. よく使うマッピングテクニック

JSONキー名が異なる場合

JSONとModelで名前が一致しないケースは稀によくあります (適当)

{
  "user_name": "Arthur"
}

var userName: String?

この場合も、mapping でキーを指定するだけOKです。

mutating func mapping(map: Map) {
    userName <- map["user_name"]
}

CodableCodingKeys よりも直感的に書けることがメリット?と感じる人もいるかもですね。


Optionalなプロパティの扱い

ObjectMapperでは、Optionalの扱いも簡単です。

var age: Int?

JSONにキーが存在しない、または null の場合でも、
クラッシュせず nil が代入されます。

mutating func mapping(map: Map) {
    age <- map["age"]
}

特別な設定は不要です。

ネストしたJSONのマッピング

実務でよく見るネスト構造です。

{
  "id": 1,
  "profile": {
    "email": "test@example.com"
  }
}

// Modelを分ける場合
struct Profile: Mappable {
    var email: String?

    init?(map: Map) {}

    mutating func mapping(map: Map) {
        email <- map["email"]
    }
}

struct User: Mappable {
    var id: Int?
    var profile: Profile?

    init?(map: Map) {}

    mutating func mapping(map: Map) {
        id      <- map["id"]
        profile <- map["profile"]
    }
}

配列のマッピング:

{
  "users": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ]
}

struct Response: Mappable {
    var users: [User]?

    init?(map: Map) {}

    mutating func mapping(map: Map) {
        users <- map["users"]
    }
}

このあたりは Codable と比べても直感的で、
ObjectMapperが「柔軟」と言われる理由の一つです。


6. ObjectMapperの便利機能

TransformTypeの使い方

ObjectMapperの強みのひとつが Transform です。
JSONとModelの型が一致しない場合に変換処理を挟めます。

Transformは TransformType プロトコルを使います。

public protocol TransformType {
    associatedtype Object
    associatedtype JSON

    func transformFromJSON(_ value: Any?) -> Object?
    func transformToJSON(_ value: Object?) -> JSON?
}

// 要するに、**「JSON → Swift」「Swift → JSON」両方向の変換を書く」**だけ

日付(Date)の変換

APIでよくあるのが、Stringで返ってくる日付です。

{
  "created_at": "2024-12-01T10:00:00Z"
}

ObjectMapperには、あらかじめ用意された DateTransform があります。

struct Article: Mappable {
    var createdAt: Date?

    init?(map: Map) {}

    mutating func mapping(map: Map) {
        createdAt <- (map["created_at"], DateTransform())
    }
}

// フォーマット指定も可能
DateTransform(dateFormat: "yyyy-MM-dd'T'HH:mm:ssZ")

CodableDateFormatter を毎回書くよりも、
マッピング箇所に変換ロジックを閉じ込められるのが特徴です。(最近のCodableもそこまで煩雑ではない記憶ですが)


カスタムTransformの例

例えば、Intで返ってくるフラグをBoolに変換したい場合。

{
  "is_active": 1
}

struct IntToBoolTransform: TransformType {
    func transformFromJSON(_ value: Any?) -> Bool? {
        guard let intValue = value as? Int else { return nil }
        return intValue == 1
    }

    func transformToJSON(_ value: Bool?) -> Int? {
        guard let value = value else { return nil }
        return value ? 1 : 0
    }
}

// 使用例
mutating func mapping(map: Map) {
    isActive <- (map["is_active"], IntToBoolTransform())
}

「変換を部品化できる」 点が、ObjectMapperの特徴。


7. Codableとの比較

書きやすさの違い

まずはシンプルなModelで比較してみます。

ObjectMapper

struct User: Mappable {
    var id: Int?
    var name: String?

    init?(map: Map) {}

    mutating func mapping(map: Map) {
        id   <- map["id"]
        name <- map["name"]
    }
}

Codable

struct User: Codable {
    let id: Int
    let name: String
}

単純な構造なら、Codableの圧勝


柔軟性の違い

JSONキー変換、型変換、ネスト処理が増えてくると差が出ます。(といってもこれは感じ方に個人差がありますが)

Codable(例)

enum CodingKeys: String, CodingKey {
    case userName = "user_name"
}

DateやBool変換も init(from:) を書く必要があり、
Modelが肥大化しやすくなります。

ObjectMapper

userName <- map["user_name"]
isActive <- (map["is_active"], IntToBoolTransform())

変換ロジックをmappingに集約できるのが強み。


既存プロジェクトでの使い分け

実務では、以下のような判断がよさそうかもです。

  • 新規開発
    • → Codable一択
  • 既存ObjectMapperプロジェクト
    • → 無理に置き換えない
  • API仕様が不安定/型揺れが激しい
    • → ObjectMapperの方が楽な場合あり

特にBFFや古いAPIを叩くアプリでは、
ObjectMapperの「許容力」が助けになるケースもあります。


今から使うなら

結論:

  • 学ぶ価値:✅(既存コード理解のため)
  • 新規採用:❌(基本はしない)
  • 保守運用:⭕️(状況次第)

ObjectMapperは
「過去の遺産」ではなく「読めると強いライブラリ」
という位置付けが一番しっくりきそうです、おそらく。


8. まとめ

  • なぜ昔はこれが選ばれていたのか
  • Codableが「進化」だと実感できる理由
  • 柔軟性と簡潔さのトレードオフ

この辺りを改めて復習できて非常に有意義な学びとなりました。

単なる「古いライブラリ」ではなく、
Swiftの歴史を理解する上でも意味のある存在と再認識できて、今後の開発でも詰まったらここに戻ってきて見直そうと思います、自分でも。