[ACCEPTED]-Make blank params[] nil-collections

Accepted answer
Score: 29

Consider what you're doing here by using 9 filters in the controller to affect how 8 a model behaves when saved or updated. I 7 think a much cleaner method would be a before_save call 6 back in the model or an observer. This way, you're 5 getting the same behavior no matter where 4 the change originates from, whether its 3 via a controller, the console or even when 2 running batch processes.

Example:

class Customer < ActiveRecord::Base
  NULL_ATTRS = %w( middle_name )
  before_save :nil_if_blank

  protected

  def nil_if_blank
    NULL_ATTRS.each { |attr| self[attr] = nil if self[attr].blank? }
  end
end

This yields 1 the expected behavior:

>> c = Customer.new
=> #<Customer id: nil, first_name: nil, middle_name: nil, last_name: nil>
>> c.first_name = "Matt"
=> "Matt"
>> c.middle_name = "" # blank string here
=> ""
>> c.last_name = "Haley"
=> "Haley"
>> c.save
=> true
>> c.middle_name.nil?
=> true
>>
Score: 8

If you just want to kill the blanks, you 1 can just do params.delete_if {|k,v| v.blank?}.

Score: 5

A good gem for handling this in the model: https://github.com/rmm5t/strip_attributes

It 2 defines a before_validation hook that trims whitespaces and 1 sets empty strings to nil.

Score: 3

before_save seems like the wrong location 5 to me, what if you want to use the value 4 before saving. So I overrode the setters 3 instead:

# include through module or define under active_record
def self.nil_if_blank(*args)
  args.each do |att|
    define_method att.to_s + '=' do |val|
      val = nil if val.respond_to?(:empty?) && val.empty?
      super(val)
    end
  end
end

#inside model
nil_if_blank :attr1, :attr2

Just to be complete I put the following 2 in lib/my_model_extensions.rb

module MyModelExtensions
  def self.included(base)
    base.class_eval do
      def self.nil_if_blank(*args)
        args.each do |att|
          define_method att.to_s + '=' do |val|
            val = nil if val.respond_to?(:empty?) && val.empty?
            super(val)
          end
        end
      end
    end
  end
end

and use it 1 like this:

class MyModel
  include MyModelExtensions
  nil_if_blank :attr1, :attr2
end
Score: 3

In the ApplicationController:

class ApplicationController < ActionController::Base

  def nilify(p)
    p.transform_values!{|v| v.present? ? v : nil }
  end

end

In your controller, modify 2 the strong parameters filter method to call 1 nilify:

class UserController < ApplicationController

  def user_params
    nilify params.require(:user).permit(:email, :name)
  end

end
Score: 2

You could do this using inject, which is 6 obvious as to what is happening.

params = params.inject({}){|new_params, kv| 
  new_params[kv[0]] = kv[1].blank? ? nil : kv[1]
  new_params
}

There is 5 also a hack you can do with merge by merging 4 with itself, and passing a block to handle 3 the new value (although this isn't really 2 the intended use for it, but it is more 1 concise)

params.merge(params){|k, v| v.blank? ? nil : v}
Score: 2

Ordinarily I would encourage functionality 20 to be moved into the model, as stated in 19 other answers this means that you will get 18 the same behavior no matter where the change 17 originates from.

However, I don't think in 16 this case it is correct. The affect being 15 noticed is purely down to not being able 14 to encode the difference between a blank 13 string and nil value in the HTTP request. For 12 this reason it should be remedied at the 11 controller level. It also means that in 10 other places it is still possible to store 9 an empty string in the model (which there 8 could be for a legitimate reason for, and 7 if not it is simple to cover with standard 6 validations).

The code I'm using to overcome 5 this problem is:

# application_controller.rb
...

def clean_params
  @clean_params ||= HashWithIndifferentAccess.new.merge blank_to_nil( params )
end

def blank_to_nil(hash)
  hash.inject({}){|h,(k,v)|
    h.merge(
      k => case v
      when Hash  : blank_to_nil v
      when Array : v.map{|e| e.is_a?( Hash ) ? blank_to_nil(e) : e}
      else v == "" ? nil : v
      end
    )
  }
end

...

I've tried to keep the code 4 as concise as possible, although readability 3 has suffered somewhat, so here is a test 2 case to demonstrate its functionality:

require "test/unit"
class BlankToNilTest < Test::Unit::TestCase

  def blank_to_nil(hash)
    hash.inject({}){|h,(k,v)|
      h.merge(
        k => case v
        when Hash  : blank_to_nil v
        when Array : v.map{|e| e.is_a?( Hash ) ? blank_to_nil(e) : e}
        else v == "" ? nil : v
        end
      )
    }
  end

  def test_should_convert_blanks_to_nil
    hash =        {:a => nil, :b => "b", :c => ""}
    assert_equal( {:a => nil, :b => "b", :c => nil}, blank_to_nil(hash) )
  end

  def test_should_leave_empty_hashes_intact
    hash =        {:a => nil, :b => "b", :c => {}}
    assert_equal( {:a => nil, :b => "b", :c => {}}, blank_to_nil(hash) )
  end

  def test_should_leave_empty_arrays_intact
    hash =        {:a => nil, :b => "b", :c => []}
    assert_equal( {:a => nil, :b => "b", :c => []}, blank_to_nil(hash) )
  end

  def test_should_convert_nested_hashes
    hash =        {:a => nil, :b => "b", :c => {:d => 2, :e => {:f => "",  :g => "",  :h => 5}, :i => "bar"}}
    assert_equal( {:a => nil, :b => "b", :c => {:d => 2, :e => {:f => nil, :g => nil, :h => 5}, :i => "bar"}}, blank_to_nil(hash) )
  end

  def test_should_convert_nested_hashes_in_arrays
    hash =        {:book_attributes => [{:name => "b", :isbn => "" },{:name => "c", :isbn => "" }], :shelf_id => 2}
    assert_equal( {:book_attributes => [{:name => "b", :isbn => nil},{:name => "c", :isbn => nil}], :shelf_id => 2}, blank_to_nil(hash))
  end

  def test_should_leave_arrays_not_containing_hashes_intact
    hash =        {:as => ["", nil, "foobar"]}
    assert_equal( {:as => ["", nil, "foobar"]}, blank_to_nil(hash))
  end

  def test_should_work_with_mad_combination_of_arrays_and_hashes
    hash =        {:as => ["", nil, "foobar", {:b => "b", :c => "",  :d => nil, :e => [1,2,3,{:a => "" }]}]}
    assert_equal( {:as => ["", nil, "foobar", {:b => "b", :c => nil, :d => nil, :e => [1,2,3,{:a => nil}]}]}, blank_to_nil(hash))
  end

end

This 1 can then be used in a controller like so:

...
@book.update_attributes(clean_params[:book])
...
Score: 2

You can use attribute_normalizer gem and use the blank normalizer 2 that will transform empty strings in nil 1 values.

Score: 1

Use the "in place" collect method (also 1 known as map!)

params[:user].collect! {|c| c == "" ? nil : c}
Score: 1

Chris,

Here is a recursive parsing of params 1 that have blanc values.

before_filter :process_params

......



private
def process_params
....
  set_blanc_values_to_nil(params)
end

# Maybe move method to ApplicationController
# recursively sets all blanc values to nil
def set_blanc_values_to_nil!(my_hash)
    my_hash.keys.each do |key|
        val = my_hash[key]
        next if val.nil?
        my_hash[key] = nil if val.is_a?(String) && val.empty?
        set_blanc_values_to_nil!(val) if val.is_a? Hash
    end
end
Score: 0

I generalized an answer and made a hook/extension 3 that can be used as an initializer. This 2 allows it to be used across multiple models. I've 1 added it as part of my ActiveRecordHelpers repo on GitHub

Score: 0

Here is how I did it.

def remove_empty_params(param, key)
  param[key] = param[key].reject { |c| c.empty? }
end

and call it with

remove_empty_params(params[:shipments], :included_clients)

No 3 need to get super tricky in the model. And 2 this way you can control which params get 1 cleaned up.

params = {
      "shipments"=>{
        "included_clients" => ["", "4"]
      }
    }

will turn into

>> params["shipments"]
=> {"included_clients" => ["4"] }
Score: 0

If you know which attributes you want to 4 encode blanks as nils for you can use the 3 following attribute setter override:

def colour=(colour)
  super(colour.blank? ? nil : colour)
end

A bit 2 bulky if you have a lot of attributes to 1 cover though.

More Related questions