56

As you already know, JSON naming convention advocates the use of camelCase and the Rails advocates the use of snake_case for parameter names.

What is the best way to convert all request's params to snake_case in a rails controller?

From this:

{
  ...
  "firstName": "John",
  "lastName": "Smith",
  "moreInfo":
  {
    "mealType": 2,
    "mealSize": 4,
    ...
  }
}

to this:

{
  ...
  "first_name": "John",
  "last_name": "Smith",
  "more_info":
  {
    "meal_type": 2,
    "meal_size": 4,
    ...
  }
}
wscourge
  • 10,657
  • 14
  • 59
  • 80
a.s.t.r.o
  • 3,261
  • 5
  • 34
  • 41

14 Answers14

93

When you’ve completed the steps below, camelCase param names submitted via JSON requests will be changed to snake_case.

For example, a JSON request param named passwordConfirmation would be accessed in a controller as params[:password_confirmation]

Create an initializer at config/initializers/json_param_key_transform.rb. This file is going to change the parameter parsing behaviour for JSON requests only (JSON requests must have the request header Content-Type: application/json).

Find your Rails version and choose the appropriate section below (find your Rails version in Gemfile.lock):

For Rails 5 and 6

For Rails 5 and 6, to convert camel-case param keys to snake-case, put this in the initializer:

# 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 }
}

For Rails 4.2 (and maybe earlier versions)

For Rails 4.2 (and maybe earlier versions), to convert camel-case param keys to snake-case, put this in the initializer:

# File: config/initializers/json_param_key_transform.rb
# Transform JSON request param keys from JSON-conventional camelCase to
# Rails-conventional snake_case:
Rails.application.config.middleware.swap(
  ::ActionDispatch::ParamsParser, ::ActionDispatch::ParamsParser,
  ::Mime::JSON => Proc.new { |raw_post|

    # Borrowed from action_dispatch/middleware/params_parser.rb except for
    # data.deep_transform_keys!(&:underscore) :
    data = ::ActiveSupport::JSON.decode(raw_post)
    data = {:_json => data} unless data.is_a?(::Hash)
    data = ::ActionDispatch::Request::Utils.deep_munge(data)
    
    # Transform camelCase param keys to snake_case:
    data.deep_transform_keys!(&:underscore)

    data.with_indifferent_access
  }
)

Final step for all Rails versions

Restart rails server.

Rory O'Kane
  • 29,210
  • 11
  • 96
  • 131
Eliot Sykes
  • 9,616
  • 6
  • 50
  • 64
  • 1
    Note that for this to work with jQuery, you must set `contentType: 'application/json'` – Eva Jul 18 '15 at 20:26
  • In Rails 5.0 ParamsParser interface will be changed. You only need `data = ActiveSupport::JSON.decode(raw_post)` and `data.is_a?(Hash) ? data : {:_json => data}`. Refer https://github.com/rails/rails/blob/b7ac0790682e0a56b406127891dfefc15a5eaa64/actionpack/lib/action_dispatch/http/parameters.rb#L7 – ypresto Feb 02 '16 at 10:28
  • 3
    Got `ActionDispatch::ParamsParser::ParseError: undefined method 'underscore' for :_json:Symbol` when Array is passed. You should skip :_json or use another container hash (i.e. `data = { '_container' => data }` then call deep_transform_keys). – ypresto Feb 02 '16 at 11:30
  • This does not work in rails 5. `assert_index': No such middleware to insert before: ActionDispatch::ParamsParser (RuntimeError)` – Vox Feb 16 '16 at 15:36
  • Rails 5.0 instructions added above in main answer. – Eliot Sykes Feb 16 '16 at 20:19
  • In Rails 5.0, I believe you need to use `Mime[:json].symbol` – Vince May 24 '16 at 11:20
  • @Sorenly can you confirm what beta/rc version of Rails 5 this change is needed in? – Eliot Sykes May 24 '16 at 19:06
  • @EliotSykes here you go https://github.com/rails/rails/commit/97ed810cfc15725a0856227fa9f9eb26930f16c8#diff-6a80fa5836403509e86a81c2117af391 – Vince May 24 '16 at 20:49
  • @Sorenly much appreciated, thanks! Snippet updated for Rails 5. – Eliot Sykes May 25 '16 at 15:30
  • 2
    @EliotSykes do you know similar solution for GET request? – vladra Jun 01 '16 at 18:17
  • @vladra what happens with GET requests when you try this? If you get errors, please provide details. – Eliot Sykes Jun 02 '16 at 16:00
  • @EliotSykes during GET request the above code is not executed at all. I checked rails docs, and made an assumption that it is working only for POST or PUT/PATCH http://edgeapi.rubyonrails.org/classes/ActionDispatch/ParamsParser.html – vladra Jun 02 '16 at 17:28
  • @vladra It may be the Content-Type header is not set to application/json – Eliot Sykes Jun 03 '16 at 07:56
  • 4
    `deep_transform_keys!` is deprecated for the `params` object in Rails 5. Instead, use `transform_keys!`, as in `params.transform_keys!(&:underscore)` ([documentation](http://api.rubyonrails.org/v5.0.0.1/classes/ActionController/Parameters.html#method-i-transform_values-21)) – django09 Sep 29 '16 at 18:22
  • @django09 Can you confirm this is an issue in the above code? In case its relevant to the deprecation, `data` is a `Hash`, not a `ActionController::Parameters` instance in the Rails 5 snippet. – Eliot Sykes Sep 30 '16 at 08:27
  • Wouldn't calling `params.deep_transform_keys!(&:underscore)` in `before_action` in the base API controller also work in a Rails 4 app? – BrunoF Aug 04 '17 at 13:14
  • Hi @BrunoFacca, thanks, agree that approach would also work. – Eliot Sykes Aug 04 '17 at 17:02
  • 2
    @BrunoFacca one problem w/calling it in a `before_action` is that the conversion happens later in the process. So if you want things like parameter wrapping (or even just logging the parameters) to work properly, you'll want to do it in an initializer. That's why I ended up here today :) – Sunil D. Oct 25 '17 at 17:23
  • this solution did not handle query string parameters, instead I implemented a variation of another [solution](https://stackoverflow.com/a/57023035/7573223) – SMAG Mar 05 '21 at 22:03
  • This is not converting my values to snake case when I write request tests. Do my test environment not load the initalizers? – aks Dec 12 '22 at 19:12
17

Example with camelCase to snake_case in rails console

2.3.1 :001 > params = ActionController::Parameters.new({"firstName"=>"john", "lastName"=>"doe", "email"=>"john@doe.com"})
=> <ActionController::Parameters {"firstName"=>"john", "lastName"=>"doe", "email"=>"john@doe.com"} permitted: false>

2.3.1 :002 > params.transform_keys(&:underscore)
=> <ActionController::Parameters {"first_name"=>"john", "last_name"=>"doe", "email"=>"john@doe.com"} permitted: false>

source:

http://api.rubyonrails.org/classes/ActionController/Parameters.html#method-i-transform_keys http://apidock.com/rails/String/underscore

UPDATE:

If you have nested attributes and Rails 6 you can do:

ActionController::Parameters convert to hash and then do deep transform:

params.permit!.to_h.deep_transform_keys { |key| key.to_s.underscore } params.permit!.to_h.deep_transform_values { |value| value.to_s.underscore }

Please see:

http://apidock.com/rails/v6.0.0/Hash/deep_transform_values http://apidock.com/rails/v6.0.0/Hash/deep_transform_keys

Hubert Olender
  • 1,130
  • 9
  • 15
  • I don't think this works in the case of nested parameters (e.g. `nested_model_attributes`) – Matt Jul 21 '20 at 14:38
  • 2
    @Matt Hello, thank you for your reply. If you use Rails 6 I think for nested attributes you can transform ActionController::Parameters to hash and then do deep transform attributes: params.permit!.to_h.deep_transform_keys { |key| key.to_s.underscore } or params.permit!.to_h.deep_transform_values { |value| value.to_s.underscore } Please see: https://apidock.com/rails/v6.0.0/Hash/deep_transform_values https://apidock.com/rails/v6.0.0/Hash/deep_transform_keys – Hubert Olender Jul 22 '20 at 07:45
15

In Rails 6.1 will be added deep_transform_keys to ActionController::Parameters so it enables you to make it as simple as:

class ApplicationController < ActionController::Base
  before_action :underscore_params!

  private

  def underscore_params!
    params.deep_transform_keys!(&:underscore)
  end
end

Edit

At the moment you can backport as follows:

module DeepTransformKeys
  def deep_transform_keys!(&block)
    @parameters.deep_transform_keys!(&block)
    self
  end
end

ActionController::Parameters.include(DeepTransformKeys)
Aleksandr K.
  • 1,338
  • 14
  • 21
  • NoMethodError undefined method `deep_transform_keys!' for # Seems like that method is defined only on Hash, and ActionController::Parameters is not a Hash. – Almaron Feb 16 '20 at 13:39
  • @Almaron, nope https://edgeapi.rubyonrails.org/classes/ActionController/Parameters.html#method-i-deep_transform_keys but it only available since Rails 6.0 – Aleksandr K. Feb 17 '20 at 06:20
  • Strange. Why did it give an error then.. I'm running 6.0.2.1 – Almaron Feb 23 '20 at 20:51
  • @Almaron I have same error on Rails-6.0.2.1. So I check source code here: https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/strong_parameters.rb#L712 => It means the method is availabe on master branch. But not available for v6.0.2.1 : https://github.com/rails/rails/blob/v6.0.2.1/actionpack/lib/action_controller/metal/strong_parameters.rb – ttuan Mar 20 '20 at 02:56
  • 2
    Ugly workaround until available: `params.instance_variable_get(:@parameters).deep_transform_keys!(&:underscore)` – vhiairrassary Apr 13 '20 at 15:32
  • I have no idea why it's not working for you guys, but working in my pet project https://github.com/skyderby/skyderby/blob/dev/app/controllers/api/application_controller.rb#L8 – Aleksandr K. Apr 14 '20 at 08:29
  • 2
    It's not available on [stable](https://github.com/rails/rails/blob/6-0-stable/actionpack/lib/action_controller/metal/strong_parameters.rb) for some reason. Will be available in 6.1 (I created an issue [here](https://github.com/rails/rails/issues/39081)). – sloneorzeszki Apr 29 '20 at 13:44
  • My bad. You guys are right, I'm wrong. I backported it to my pet project and forgot about it https://github.com/skyderby/skyderby/blob/aea0119a2a94f0a93679372fcbd064e24d50dc3d/config/initializers/params_patch.rb – Aleksandr K. Apr 30 '20 at 09:53
8

ActiveSupport already provides a String#snakecase method. All you have to do is install a filter that does a deep iteration through the params hash and replaces the keys with key.snakecase.

before_filter :deep_snake_case_params!

def deep_snake_case_params!(val = params)
  case val
  when Array
    val.map {|v| deep_snake_case_params! v }
  when Hash
    val.keys.each do |k, v = val[k]|
      val.delete k
      val[k.snakecase] = deep_snake_case_params!(v)
    end
    val
  else
    val
  end
end
Chris Heald
  • 61,439
  • 10
  • 123
  • 137
8

Merging Sebastian Hoitz's answer with this gist, I could make it work on rails 4.2, strong parameters AND parameters wrapping with the wrap_parameters tagging method.

I couldn't make it work using a before_filter, probably because the parameter wrapping is done before filtering.

In config/initializers/wrap_parameters.rb:

# Convert json parameters, sent from Javascript UI, from camelCase to snake_case.
# This bridges the gap between javascript and ruby naming conventions.
module ActionController
  module ParamsNormalizer
    extend ActiveSupport::Concern

    def process_action(*args)
      deep_underscore_params!(request.parameters)
      super
    end

    private
      def deep_underscore_params!(val)
        case val
        when Array
          val.map {|v| deep_underscore_params! v }
        when Hash
          val.keys.each do |k, v = val[k]|
            val.delete k
            val[k.underscore] = deep_underscore_params!(v)
          end
          val
        else
          val
        end
      end
  end
end

# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
ActiveSupport.on_load(:action_controller) do
  wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
  # Include the above defined concern
  include ::ActionController::ParamsNormalizer
end
petRUShka
  • 9,812
  • 12
  • 61
  • 95
patatepartie
  • 178
  • 1
  • 6
  • Your solution was only for POST requests. It is better to change `request_parameters` to `parameters`. In that case it will work for both. I've fix it. http://api.rubyonrails.org/classes/ActionDispatch/Request.html#method-i-request_parameters – petRUShka Aug 13 '16 at 16:29
6

Solution for Rails 5

before_action :underscore_params!

def underscore_params!
  underscore_hash = -> (hash) do
    hash.transform_keys!(&:underscore)
    hash.each do |key, value|
      if value.is_a?(ActionController::Parameters)
        underscore_hash.call(value)
      elsif value.is_a?(Array)
        value.each do |item|
          next unless item.is_a?(ActionController::Parameters)
          underscore_hash.call(item)
        end
      end
    end
  end
  underscore_hash.call(params)
end
6

I couldn't use other suggestions here directly, but it got me on the right track.

With Rails 5.2, using a versioned API and thus unable to change it for the whole application. I created this concern which i then included into the base controller of my new api version module.

module UnderscoreizeParams
  extend ActiveSupport::Concern

  def process_action(*args)
    request.parameters.deep_transform_keys!(&:underscore)
    super
  end
end

then in my API V3 BaseController

class V3::BaseController
  include UnderscoreizeParams
end

enjoy.

Salz
  • 152
  • 1
  • 8
  • This solution worked best for me, as I wanted to uniformly convert all params keys into underscores from pascalCase / CamelCase(:lower). Some of the other solutions did not handle query string parameters, where this solution does handle query string parameters. – SMAG Mar 05 '21 at 22:01
4

Another rails 5.1 solution that piggy backs off of the Sebastian Hoitz solution above. To clarify why we need to do this: in R5.1 deep_transform_keys! is no longer a method available to us, since params are no longer inheriting from HashWithIndifferentAccess. And overcomes the issue mentioned by Eliot Sykes where the initializer only works for application/json mime types. It does add overhead to all the requests though. (I'd love to see some initializers for ActionDispatch::Request.parameter_parsers[:multipart_form]) though, since the initializer is a better place to be doing this, IMO.

before_action :normalize_key!

 def normalize_keys!(val = params)
  if val.class == Array
    val.map { |v| normalize_keys! v }
  else
    if val.respond_to?(:keys)
      val.keys.each do |k|
        current_key_value = val[k]
        val.delete k
        val[k.to_s.underscore] = normalize_keys!(current_key_value)
      end
    end
    val
  end
  val
end
Mike
  • 601
  • 6
  • 4
3

We are converting our Rails API JSON keys from snake_case to camelCase. We have to do the conversion incrementally, i.e. some APIs work with snake_case while the others change to using camelCase.

Our solution is that we

  • create method ActionController::Parameters#deep_snakeize
  • create method ApplicationController#snakeize_params
  • set before_action :snakeize_params only for the controller actions that handle incoming request with camelCase keys

You can try vochicong/rails-json-api for a fully working Rails app example.

# File: config/initializers/params_snakeizer.rb
# Transform JSON request param keys from JSON-conventional camelCase to
# Rails-conventional snake_case
module ActionController
  # Modified from action_controller/metal/strong_parameters.rb
  class Parameters
    def deep_snakeize!
      @parameters.deep_transform_keys!(&:underscore)
      self
    end
  end
end

# File: app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  protected

    # Snakeize JSON API request params
    def snakeize_params
      params.deep_snakeize!
    end
end

class UsersController < ApplicationController
  before_action :snakeize_params, only: [:create]

  # POST /users
  def create
    @user = User.new(user_params)

    if @user.save
      render :show, status: :created, location: @user
    else
      render json: @user.errors, status: :unprocessable_entity
    end
  end
end
vochicong
  • 51
  • 4
2

I wanted to use Chris Healds version, but since I am using Rails 4 I have strong_parameters enabled so I had to change it up a bit.

This is the version that I came up with:

before_filter :deep_underscore_params!


def deep_underscore_params!(val = request.parameters)
  case val
  when Array
    val.map { |v| deep_underscore_params!(v) }
  when Hash
    val.keys.each do |k, v = val[k]|
      val.delete k
      val[k.underscore] = deep_underscore_params!(v)
    end

    params = val
  else
    val
  end
end
Sebastian Hoitz
  • 9,343
  • 13
  • 61
  • 77
0

You can create a filter that runs before any controller call and apply the following instructions to it:

# transform camel case string into snake case
snake_string  = Proc.new {|s| s.gsub(/([a-z])([A-Z])/) {|t| "#{$1}_#{$2.downcase}"}} 

# transform all hash keys into snake case
snake_hash    = Proc.new do |hash| 
  hash.inject({}) do |memo, item|
    key, value = item

    key = case key
          when String
            snake_string.call(key)
          when Symbol
            snake_string.call(key.to_s).to_sym
          else 
            key
          end    

    memo[key] = value.instance_of?(Hash) ? snake_hash.call(value) : value
    memo
  end
end

params = snake_hash.call(params)

You must have to consider the above procedure will impose a small overhead on every Rails call.

I am not convinced this is necessary, if it is just to fit in a convention.

Thiago Lewin
  • 2,810
  • 14
  • 18
  • 3
    Can also make use of ActiveSupport's underscore method http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-underscore – Puhlze Jun 21 '13 at 18:02
  • @Puhlze Thanks! I didn't know about it. – Thiago Lewin Jun 21 '13 at 18:14
  • There must be a cleaner way to change params hash using underscore method, I'll try to hack something together unless someone beat me to it. – a.s.t.r.o Jun 23 '13 at 21:51
0

tlewin's answer didn't work for me in Rails 3. it seems the params' = operator renders future operations on it void. very strange. anyways the following works for me, as it only uses the []= and delete operators:

before_filter :underscore_param_keys
def underscore_param_keys
  snake_hash = ->(hash) {
    # copying the hash for iteration so we are not altering and iterating over the same object
    hash.to_a.each do |key, value|
      hash.delete key
      hash[key.to_s.underscore] = value
      snake_hash.call(value) if value.is_a? Hash
      value.each { |item| snake_hash.call(item) if item.is_a? Hash } if value.is_a? Array
    end
  }
  snake_hash.call(params)
end
jakeonfire
  • 75
  • 4
0

you can try this:

class ApplicationController < ActionController::API
  include ControllerHelper
  before_action :deep_underscore_params!

  def deep_underscore_params!(app_params = params)
    app_params.transform_keys!(&:underscore)
    app_params.each do |key, value|
      deep_underscore_params!(value) if value.instance_of?(ActionController::Parameters)
    end
    app_params.reject! { |k, v| v.blank? }
  end
end
0

Riffing on Eliot Sykes's answer above, I think we can do a bit better in the Rails5 case. I don't love overwriting that function entirely, since that code could change. So instead I suggest using function composition:

# 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] = (
  # Compose the original parser with a transformation
  ActionDispatch::Request.parameter_parsers[:json] >>
    # Transform camelCase param keys to snake_case
    ->(data) {
      data.deep_transform_keys(&:underscore)
    }
)
hmayer
  • 1
  • 3