I came across an interesting problem in one of my ActiveRecord models (paraphrased, this isn’t the exact model):

class Event < ActiveRecord::Base
  attr_accessible :certainty
  validates :certainty, :inclusion => {
    :in => %w(less neutral more),
    :message => "%{value}"
  }
end

The problem is I would set certainty to one of the accepted values, let’s say 'less', and the form would wind up throwing an error. I overrode the default error message just to retrieve the value and it turns out the value for certainty is 0.

The reason this was happening is because certainty is defined as a PostgreSQL enumerated type:

  CREATE TYPE certainty AS ENUM ('less', 'neutral', 'more');

and these are the type detection methods in ActiveRecord:

From lib/active_record/connection_adapters/postgresql_adapter.rb:

def simplified_type(field_type)
  case field_type
  # Numeric and monetary types
  when /^(?:real|double precision)$/
    :float
  # Monetary types
  when 'money'
    :decimal
  when 'hstore'
    :hstore
  # Network address types
  when 'inet'
    :inet
  when 'cidr'
    :cidr
  when 'macaddr'
    :macaddr
  # Character types
  when /^(?:character varying|bpchar)(?:\(\d+\))?$/
    :string
  # Binary data types
  when 'bytea'
    :binary
  # Date/time types
  when /^timestamp with(?:out)? time zone$/
    :datetime
  when 'interval'
    :string
  # Geometric types
  when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
    :string
  # Bit strings
  when /^bit(?: varying)?(?:\(\d+\))?$/
    :string
  # XML type
  when 'xml'
    :xml
  # tsvector type
  when 'tsvector'
    :tsvector
  # Arrays
  when /^\D+\[\]$/
    :string
  # Object identifier types
  when 'oid'
    :integer
  # UUID type
  when 'uuid'
    :uuid
  # Small and big integer types
  when /^(?:small|big)int$/
    :integer
  # Pass through all types that are not specific to PostgreSQL.
  else
    super
  end
end

and lib/active_record/connection_adapters/column.rb:

def simplified_type(field_type)
  case field_type
  when /int/i
    :integer
  when /float|double/i
    :float
  when /decimal|numeric|number/i
    extract_scale(field_type) == 0 ? :integer : :decimal
  when /datetime/i
    :datetime
  when /timestamp/i
    :timestamp
  when /time/i
    :time
  when /date/i
    :date
  when /clob/i, /text/i
    :text
  when /blob/i, /binary/i
    :binary
  when /char/i, /string/i
    :string
  when /boolean/i
    :boolean
  end
end

The field_type in the above method calls is 'certainty.' The simplified_type in PostgreSQLColumn doesn’t match on any of the cases and it gets passed to the parent. In the parent, Column, 'certainty' matches against 'when /int/i' and returns integer as a type. Once the integer is set as the type, ActiveRecord does its thing and converts the attribute to an integer prior to a save. The validation then triggers on the changed value.

I couldn’t figure out where the variable actually gets type checked and changed to the correct type value. I tried modifying the column type directly in the model but that didn’t prevent the variable from being converted. My solution was to simply ALTER TYPE certainty RENAME TO certain. Postgre cascaded the changes and everything ended up being okay.

I want to thank Joe for taking the lead and filing a bug report here: https://github.com/rails/rails/issues/7814.