22
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Systemi(株式会社システムアイ)Advent Calendar 2024

Day 4

qs.parse() 互換実装でわかる qs.parse() の仕様

Last updated at Posted at 2024-12-03

以下のようなクエリパラメータがあるとします。

aaa=bbb&ccc[]=ddd&ccc[]=eee&aaa=fff&aaa=&ggg=hhh

以下のようなパラメータが渡されています。

  • aaa は bbb、fff と空白文字列
  • ccc[] は ddd、eee
  • ggg は hhh

これを qs というクエリ文字列パーサで解析しつつ、このパーサと同じ動作をする実装を行い qs の仕様を確認してみました。

TL;DR

  • 配列パラメータ foo[]=bar は配列として扱われる
  • 同一名の複数パラメータも配列として扱われる
  • つまり aaa=bbb&aaa=cccaaa[]=bbb&aaa[]=ccc は qs では同じ扱いだった
  • 2024 年 12 月現在、aaa=bbb&aaa=cccaaa[]=bbb&aaa[]=ccc では aaa=bbb&aaa=ccc の方が仕様として良さそう

実装

import qs from 'qs'

const query = 'aaa=bbb&ccc%5B%5D=ddd&ccc%5B%5D=eee&aaa=fff&aaa=&ggg=hhh'

const parse = query => {
  return query.split('&').map(param => {
    const splitted = param.split('=')
    return {
      key: decodeURIComponent(splitted[0]),
      value: decodeURIComponent(splitted[1])
    }
  }).reduce((acc, cur) => {
    if (cur.key.endsWith("[]")) {
      const key = cur.key.substr(0, cur.key.length - 2)
      if (acc[key] === undefined) {
        acc[key] = []
      }
      acc[key].push(cur.value)
    } else {
      if (acc[cur.key] === undefined) {
        acc[cur.key] = cur.value
      } else if (Array.isArray(acc[cur.key])) {
        acc[cur.key].push(cur.value)
      } else {
        acc[cur.key] = [acc[cur.key], cur.value]
      }
    }
    return acc
  }, {})
}

console.log(qs.parse(query))  // { aaa: [ 'bbb', 'fff', '' ], ccc: [ 'ddd', 'eee' ], ggg: 'hhh' }
console.log(parse(query))  // { aaa: [ 'bbb', 'fff', '' ], ccc: [ 'ddd', 'eee' ], ggg: 'hhh' }

まとめ

qs の互換実装を確認してみましたが、多くのプログラミング言語やフレームワークでも aaa=bbb&aaa=cccaaa[]=bbb&aaa[]=ccc は同じ扱いですが、異なる言語やフレームワークもあるので注意が必要です(以下リスト参照)。一般には aaa=bbb&aaa=ccc が扱いがシンプルでしょう。

  • Node.js の Express はどちらも req.query.aaa['bbb', 'ccc'] を返す
  • PHP はどちらも $_GET['aaa']['bbb', 'ccc'] を返す
  • Python の Flask はどちらも request.args['aaa']['bbb', 'ccc'] を返す
  • Python の Django はどちらも request.GET.getlist('aaa')['bbb', 'ccc'] を返す
  • Ruby on Rails はどちらも params[:aaa]['bbb', 'ccc'] を返す
  • Go の net/http はどちらも r.URL.Query()["aaa"]["bbb", "ccc"] を返す
  • Go の Gin はどちらも c.DefaultQueryArray("aaa")["bbb", "ccc"] を返す
  • Java の Spring Boot はどちらも @RequestParam("aaa")["bbb", "ccc"] を返す
  • Haskell の warp は aaaaaa[] の書き分けが必要だがどちらも ["bbb", "ccc"] を返す
    • lookup "aaa" $ queryString request
    • lookup "aaa[]" $ queryString request
  • Rust の warp は aaaaaa[] の書き分けが必要だがどちらも ["bbb", "ccc"] を返す
    • warp::get().and(warp::path::end()).and(warp::query::<std::collections::HashMap<String, Vec<String>>>()).map(|params: std::collections::HashMap<String, Vec<String>>| { if let Some(values) = params.get("aaa") { values } else { [] } })
    • warp::get().and(warp::path::end()).and(warp::query::<std::collections::HashMap<String, Vec<String>>>()).map(|params: std::collections::HashMap<String, Vec<String>>| { if let Some(values) = params.get("aaa[]") { values } else { [] } })
22
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?