ActiveRecordでpostgresqlのjson typeのバリデーションを行う


ActiveRecordは4.2以降でPostgreSQLのJSON型をサポートしています。

# == Schema Information
#
# Table name: hoges
#
#  id       :integer          not null, primary key
#  name     :string
#  options  :json
#

class Hoge < ActiveRecord::Base
end

このHogeモデルのoptionsにJSON形式の文字列を渡すと正しく保存されるのですが、もしJSON形式でない文字列が渡ると、値にnilが設定されただけで何もエラーが起きませんでした。

バリデーションをしようにも、不正なJSON文字列を渡した時点で既にnilになってしまうため、困ってしまいました。
pryを使って調べたところ、不正なJSON文字列は、options_before_type_castという属性に保存されていました。今回はこれを使って、JsonTypeValidatorを作ってみました。

JsonTypeValidatorの定義

lib/validators/json_type_validator.rb
class JsonTypeValidator < ActiveModel::EachValidator

  def initialize(options)
    options.reverse_merge!(:message => :invalid)
    super(options)
  end

  def validate_each(record, attribute, value)
    value = record.try(:"#{attribute}_before_type_cast")
    value = value.strip if value.is_a?(String)
    return true if value.blank?
    if value.is_a? String
      ActiveSupport::JSON.decode(value)
    else
      true
    end
  rescue JSON::ParserError => exception
    record.errors.add(attribute, options[:message], exception_message: exception.message)
  end
end

指定されたattributeに該当するbefore_type_castの値を取得し、それが文字列であれば、JSON.decodeを実行しています。パースに失敗したらエラーの原因をexception_messageに渡しています。基本的には、エラーメッセージは:invalidで不正なデータとして扱っています。

使い方

config/application.rbで読み込むように設定します。

config/application.rb
config.autoload_paths += %W(#{config.root}/lib/validators)

次に、Modelに定義します。

# == Schema Information
#
# Table name: hoges
#
#  id       :integer          not null, primary key
#  name     :string
#  options  :json
#

class Hoge < ActiveRecord::Base
  validates :options, json_type: true
end

これで、不正なJSON文字列が渡った場合はエラーになるようになりました。

gistで公開しています

今回のコードはgistでも公開しています。
https://gist.github.com/patorash/8f79c1dc9e271132a2152808b1bce477

また、以下のURLを参考にしました。
このURLは、カラムが文字列型でJSONを保存する場合の方法だったので、カラムの型がJSON型の場合はうまくいきませんでしたが、とても参考になりました。

参考にしたURL:
- http://stackoverflow.com/questions/6594257/how-to-validate-if-a-string-is-json-in-a-rails-model
- https://gist.github.com/joost/7ee5fbcc40e377369351