Railsでキャメルケースのリクエストを扱うために
2020.10.12
RailsでAPIを実装する場合にRailsのコードではスネークケースを使いつつ、クライアントにはキャメルケースのリクエスト(あるいはレスポンス)を扱っているように振る舞うための方法を調べました。
背景
過去の記事でOpenAPIをベースにした開発を進めているという話題がありました。
そのなかでフロントの組み込みの段階でリクエストがあったのが、「リクエスト、レスポンスのJSONのキーをキャメルケースにして欲しい」というものでした。
簡単に説明すると、上の例がスネークケースで一般的にRubyで変数を扱うときによく使われます。
一方で下の例はキャメルケースと呼ばれ、JavaScriptなどでよく使われます。
{"first_name": "foo", "last_name": "bar", "note": "snake case"}
{"firstName": "foo", "lastName": "bar", "note": "camel case"}
※変数名の命名規則は基本的に絶対的なルールがあるものではないですが、おおよそプログラミング言語などによって好まれるパターンがあります
Railsで素直にJSONを返すようなAPIサーバーを実装をするとリクエストもレスポンスもキーは基本的にスネークケース。
しかしフロントエンドではキーにキャメルケースを使いたいし、いちいちリクエストでデータを投げる場合やレスポンスをパースする場合に変換するのは面倒。
ということでRailsでリクエスト、レスポンスのJSONのキーをキャメルケースとして上手く扱う方法を調べてみました。
やったこと
前提として以下のようなことを意識しつつ、レスポンスとリクエストについてそれぞれ方法を考えてみました。
- Railsのコード内では可能な限りスネークケースを使いたい
- キャメルケースとスネークケースの変換をするコードはできるだけ集約したい
- パフォーマンスに大きく影響しない
レスポンス
順番的にはリクエストから書くべきかもしれませんが、レスポンスを扱う場合のほうが簡単だったので先に紹介します。
ただし前提としてActiveModelSerialzersを使っているとします。
ActiveModelSerializerを使っている場合は key_transform
を指定することで簡単にレスポンスのキーのパターンを変えることができます。
以下のドキュメントを参考に、今回のケースであれば :camel_lower
を指定するだけでOKです。
リクエスト
先に挙げた例のようにPOSTリクエストでキーがキャメルケースのJSONを送ったとします。
{"firstName": "foo", "lastName": "bar", "note": "camel case"}
これをRailsで処理するときに以下のようにキーがスネークケースのデータになっていてほしいとするとどうするのがよいでしょうか。
{"first_name": "foo", "last_name": "bar", "note": "snake case"}
できればControllerでリクエストパラメータを扱う時点で変換された状態になっていてほしい、ということで調べてみたところStack Overflowに同じ趣旨の質問と回答がありました。
- Stack Overflow: What is the best way to convert all controller params from camelCase to snake_case in Rails?
上記記事より抜粋したのが以下のコードです。(Railsのバージョンは6とします)
# File: config/initializers/json_param_key_transform.rb
# Transform JSON request param keys from JSON-conventional camelCase to
# Rails-conventional snake_case:
ActionDispatch::Request.parameter_parsers[:json] = lambda { |raw_post|
# Modified from action_dispatch/http/parameters.rb
data = ActiveSupport::JSON.decode(raw_post)
# Transform camelCase param keys to snake_case
if data.is_a?(Array)
data.map { |item| item.deep_transform_keys!(&:underscore) }
else
data.deep_transform_keys!(&:underscore)
end
# Return data
data.is_a?(Hash) ? data : { '_json': data }
}
とりあえず書いてあるとおりに試したところ、JSONのキーがキャメルケースからスネークケースに変換されたリクエストパラメータを受け取ることができました。
一体このコードは何をしているのでしょうか?
Modified from action_dispatch/http/parameters.rb
このコメントを参考にRailsのコードを確認してみました。
- GitHub: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/parameters.rb
このなかで DEFAULT_PARSERS
が定義されており、JSONをパースするメソッドが定義されているようです。
DEFAULT_PARSERS = {
Mime[:json].symbol => -> (raw_post) {
data = ActiveSupport::JSON.decode(raw_post)
data.is_a?(Hash) ? data : { _json: data }
}
}
つまり記事の中で紹介しているコードはJSONをパースするメソッドを deep_transform_keys!(&:underscore)
を使って上書きすることで、パースしたパラメータのキーを強制的にスネークケースにするためのものと理解しました。
まとめ
紹介したようにリクエスト、レスポンスに関する設定を行うことでRailsではキーをスネークケースで扱いつつ、APIにリクエストするクライアントにはキーがスネークケースであるように振る舞うことができるようになりました。
当然キャメルケース以外でも設定を変更することで対応可能なので、状況に応じて使い分けが簡単にできそうです。
JSON以外のパラメータ
Railsの扱うパラメータはリクエストパラメータ以外にパスパラメータとクエリパラメータがあります。
パスパラメータはURLに含まれておりキーを指定するものではないので、Railsのルーティングでスネークケースとして定義されていれば問題はありません。
一方でクエリパラメータはクライアントでキーを指定するため同様の問題が起こりえるのですが、上記の変換する設定では変換されません。
# クライアントはuserIdとするがRailsではuser_idとして扱いたい
GET /notes?userId=1
上記の例は適当なのでクエリパラメータを使わずにパス設計を見直すことで解決できるかもしれません。
しかし、クエリパラメータが必要になったときにクライアントに部分的にスネークケースを使うように強要したり、キーの命名に1単語しか使えないという制約が発生するのは避けたいです。
このあたりを上手く解決する方法は引き続き検討していこうかと思っています。
追記
あらためて調べてみると before_action
を使ってパラメータ全体を変換するアプローチができるようです。
なんとなく最初にイメージしていたのはこちらに近く、この方が方法としては分かりやすいように思いました。(未検証)
- UQID TECH: snake_case vs camelCase: the Ruby on Rails approach
- Qiita: Rails APIでcamelCase
まだどのアプローチをとるかは決めかねているので、探りつつ決めていきたいです。