背景

過去の記事で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に同じ趣旨の質問と回答がありました。

上記記事より抜粋したのが以下のコードです。(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のコードを確認してみました。

このなかで 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 を使ってパラメータ全体を変換するアプローチができるようです。
なんとなく最初にイメージしていたのはこちらに近く、この方が方法としては分かりやすいように思いました。(未検証)

まだどのアプローチをとるかは決めかねているので、探りつつ決めていきたいです。