アイリッジ開発者ブログ

アイリッジに所属するエンジニアが技術情報を発信していきます。

Moya でクエリパラメータとして辞書型や配列型などを指定するときどうなるか

著作者:roserodionova/出典:Freepik

開発部第一グループ 山崎です。

Moyaは、iOSアプリ開発におけるAPIクライアントのための最も一般的なライブラリです。APIを呼び出すサーバーと通信する場合、一般的にクエリパラメータを渡します。

https://example.com?name=kent&tel=12345678

このURLの場合、

?name=kent&tel=12345678

がサーバに渡すクエリパラメータです。

Moyaにおいては、TargetTypeを継承するクラスで、以下のようにクエリパラメータを設定することができます。

struct MyTargetType: TargetType {
      var task: Task {
        .requestParameters(
            parameters: parameters,
            encoding: URLEncoding.queryString
        )
      }

    private var parameters: [String: Any] {
        var param: [String: Any] = [:]
        param["name"] = "kent"
        param["tel"] = 12345678
        return param
    }
}

// 得られるURL: https://example.com?name=kent&tel=12345678

配列型

URLEncoding.queryString

しかし、クエリパラメータとして何かの配列を渡すにはどうすればいいでしょうか?実は、URLEncoding.queryStringを使って渡すことができます。

struct MyTargetType: TargetType {
      var task: Task {
        .requestParameters(
            parameters: parameters,
            encoding: URLEncoding.queryString
        )
      }

    private var parameters: [String: Any] {
        var param: [String: Any] = [:]
        param["name"] = ["Bob", "George", "Tom"]
        return param
    }
}

// 得られるURL: https://example.com?name[]=Bob&name[]=George&name[]=Tom

結果として、パラメータ名と角括弧を配列要素に持つURLが得られます。

arrayEncoding: .brackets

さて、上でのセクションで述べた動作は、URLEncodingのイニシャライザを使用し、arrayEncodingの引数に明示的に.bracketsを渡した場合と同じです。

struct MyTargetType: TargetType {
      var task: Task {
        .requestParameters(
            parameters: parameters,
            encoding: URLEncoding(
              destination: .queryString,
              arrayEncoding: .brackets  
            )
        )
      }

    private var parameters: [String: Any] {
        var param: [String: Any] = [:]
        param["name"] = ["Bob", "George", "Tom"]
        return param
    }
}

// 得られるURL: https://example.com?name[]=Bob&name[]=George&name[]=Tom

arrayEncoding: .indexInBrackets

ではURLEncodingのイニシャライザに他の値を渡すとどうなるでしょうか? .indexInBracketsを渡すと、角括弧内にインデックス番号が出て来るようになります。これは、Node.jsやjQueryの動作と一致します。

struct MyTargetType: TargetType {
      var task: Task {
        .requestParameters(
            parameters: parameters,
            encoding: URLEncoding(
              destination: .queryString,
              arrayEncoding: .indexInBrackets  
            )
        )
      }

    private var parameters: [String: Any] {
        var param: [String: Any] = [:]
        param["name"] = ["Bob", "George", "Tom"]
        return param
    }
}

// 得られるURL: https://example.com?name[0]=Bob&name[1]=George&name[2]=Tom

arrayEncoding: .noBrackets

.noBracketsを渡すと、角括弧は無くなります。パラメータ名が繰り返されるだけとなります。

struct MyTargetType: TargetType {
      var task: Task {
        .requestParameters(
            parameters: parameters,
            encoding: URLEncoding(
              destination: .queryString,
              arrayEncoding: .noBrackets  
            )
        )
      }

    private var parameters: [String: Any] {
        var param: [String: Any] = [:]
        param["name"] = ["Bob", "George", "Tom"]
        return param
    }
}

// 得られるURL: https://example.com?name=Bob&name=George&name=Tom

なお、ParameterEncodingですが、元の定義は Alamofire.URLEncoding で宣言されたものをtypealiasで参照しています。

github.com もし詳しいソースを見たい場合は、 Alamofireをご覧ください。

辞書型

arrayEncoding は関係なし

では、辞書をクエリパラメータとして使用する場合はどうなるでしょうか?.bracketsを渡すと、辞書のキーを含む角括弧が出力されます。
また、代わりに.indexInBracketsや.noBracketsを渡したとしても、実は結果は変わらず同じURLを出力します。というのも、辞書にはインデックス番号がないからです。

struct MyTargetType: TargetType {
      var task: Task {
        .requestParameters(
            parameters: parameters,
            encoding: URLEncoding(
              destination: .queryString,
              arrayEncoding: .brackets// Same behavior with .indexInBrackets and .noBrackets  
            )
        )
      }

    private var parameters: [String: Any] {
        var param: [String: Any] = [:]
        param["capital"] = [
            "US": "Washington",
            "Japan": "Tokyo",
            "China": "Beijing",
        ]
        return param
    }
}

// 得られるURL: https://example.com?capital[US]=Washington&capital[Japan]=Tokyo&capital[China]=Beijing

配列内の辞書型

arrayEncoding: .brackets

次に、辞書を含む配列をクエリパラメータとして渡すとどうなるでしょうか?
.bracketsストラテジーを使用すると、2つの角括弧を持つ名前のクエリパラメータを取得できます。最初の括弧は空で、2番目の括弧は辞書のキーを持ちます。

struct MyTargetType: TargetType {
      var task: Task {
        .requestParameters(
            parameters: parameters,
            encoding: URLEncoding(
              destination: .queryString,
              arrayEncoding: .brackets
            )
        )
      }

    private var parameters: [String: Any] {
        var param: [String: Any] = [:]
        param["person"] = [
            [
                "name": "Kent",
                "tel": "12345",
            ],
            [
                "name": "Bob",
                "tel": "67890",
            ]
        ]
        return param
    }
}

// 得られるURL: https://example.com?person[][name]=Kent&person[][tel]=12345&person[][tel]=67890&person[][name]=Bob

しかし、この方法はあまりお勧めできません。nameとtelの辞書キーの関連性が失われてしまうからです。クエリパラメータの順番はランダムなので、サーバーサイドはKentとBobのどちらが 「12345 」という電話番号を所有しているのかを知ることができません。

arrayEncoding: .noBrackets

.noBracketsを使用すると、配列のインデックスを示す括弧がなくなります。その結果、各パラメータに1組の括弧がつくことになります。その結果、いくつかの情報が失われます。これもまたお勧めしません。

struct MyTargetType: TargetType {
      var task: Task {
        .requestParameters(
            parameters: parameters,
            encoding: URLEncoding(
              destination: .queryString,
              arrayEncoding: .indexInBrackets
            )
        )
      }

    private var parameters: [String: Any] {
        var param: [String: Any] = [:]
        param["person"] = [
            [
                "name": "Kent",
                "tel": "12345",
            ],
            [
                "name": "Bob",
                "tel": "67890",
            ]
        ]
        return param
    }
}

// 得られるURL: https://example.com?person[name]=Kent&person[tel]=12345&person[tel]=67890&person[name]=Bob

arrayEncoding: .indexInBrackets

arrayEncoding: .indexInBrackets を代わりに使用すると、各クエリパラメータの最初の括弧内にインデックス番号が表示されます。これは、どの電話番号がどの人物に関連付けられているかを理解するのに役立ちます。この方法をお勧めします。

struct MyTargetType: TargetType {
      var task: Task {
        .requestParameters(
            parameters: parameters,
            encoding: URLEncoding(
              destination: .queryString,
              arrayEncoding: .indexInBrackets
            )
        )
      }

    private var parameters: [String: Any] {
        var param: [String: Any] = [:]
        param["person"] = [
            [
                "name": "Kent",
                "tel": "12345",
            ],
            [
                "name": "Bob",
                "tel": "67890",
            ]
        ]
        return param
    }
}

// 得られるURL: https://example.com?person[0][name]=Kent&person[0][tel]=12345&person[1][tel]=67890&person[1][name]=Bob

辞書内の配列型

最後に、配列を持つ辞書を使う場合はどうでしょうか? 結果は想像がつくかもしれません。Array Encodingの設定は、配列のパラメータにのみ適用され、インデックスの数字がついたりつかなかったりします。配列以外のパラメータは、辞書の要素であるため、角括弧を1つだけ持ち、インデックスの数字はつきません。

struct MyTargetType: TargetType {
      var task: Task {
        .requestParameters(
            parameters: parameters,
            encoding: URLEncoding(
              destination: .queryString,
              arrayEncoding: .brackets
            )
        )
      }

    private var parameters: [String: Any] {
        var param: [String: Any] = [:]
        param["person"] = [
            "tel": 12345,
            "name": "Kent",
            "jobs": ["ios_app_engineer", "android_app_engineer"]
        ]
        return param
    }
}

// 得られるURL: https://example.com?person[tel]=12345&person[jobs][]=ios_app_engineer&person[jobs][]=android_app_engineer&person[name]=Kent
//  .indexInBracketsの場合: ?person[jobs][0]=ios_app_engineer&person[jobs][1]=android_app_engineer&person[name]=Kent&person[tel]=12345
// .noBracketsの場合: ?person[jobs]=ios_app_engineer&person[jobs]=android_app_engineer&person[name]=Kent&person[tel]=12345

結論

APIコールのために適切なクエリパラメータの設定を選択しましょう。APIの仕様をよく読んで、渡すクエリパラメータをサーバーに理解させるために、どんな方法を選ぶか考える必要があります。

調査環境

  • Xcode15.0.1
  • Moya 15.0.0