I'm building a simple app and want to be able to store json strings in a db. I have a table Interface with a column json, and I want my rails model to validate the value of the string. So something like:
class Interface < ActiveRecord::Base
attr_accessible :name, :json
validates :name, :presence => true,
:length => { :minimum => 3,
:maximum => 40 },
:uniqueness => true
validates :json, :presence => true,
:type => json #SOMETHING LIKE THIS
:contains => json #OR THIS
end
How do I do that?
I suppose you could parse the field in question and see if it throws an error. Here's a simplified example (you might want to drop the double bang for something a bit clearer):
require 'json'
class String
def is_json?
begin
!!JSON.parse(self)
rescue
false
end
end
end
Then you could use this string extension in a custom validator.
validate :json_format
protected
def json_format
errors[:base] << "not in json format" unless json.is_json?
end
Currently (Rails 3/Rails 4) I would prefer a custom validator. Also see https://gist.github.com/joost/7ee5fbcc40e377369351.
# Put this code in lib/validators/json_validator.rb
# Usage in your model:
# validates :json_attribute, presence: true, json: true
#
# To have a detailed error use something like:
# validates :json_attribute, presence: true, json: {message: :some_i18n_key}
# In your yaml use:
# some_i18n_key: "detailed exception message: %{exception_message}"
class JsonValidator < ActiveModel::EachValidator
def initialize(options)
options.reverse_merge!(:message => :invalid)
super(options)
end
def validate_each(record, attribute, value)
value = value.strip if value.is_a?(String)
ActiveSupport::JSON.decode(value)
rescue MultiJson::LoadError, TypeError => exception
record.errors.add(attribute, options[:message], exception_message: exception.message)
end
end
The best way is to add a method to the JSON module !
Put this in your config/application.rb :
module JSON
def self.is_json?(foo)
begin
return false unless foo.is_a?(String)
JSON.parse(foo).all?
rescue JSON::ParserError
false
end
end
end
Now you'll be enable to use it anywhere ('controller, model, view,...'), just like this :
puts 'it is json' if JSON.is_json?(something)
I faced another problem using Rails 4.2.4 and PostgreSQL adapter (pg) and custom validator for my json field.
In the following example:
class SomeController < BaseController
def update
#record.json_field = params[:json_field]
end
end
if you pass invalid JSON to
params[:json_field]
it is quietly ignored and "nil" is stored in
#record.json_field
If you use custom validator like
class JsonValidator < ActiveModel::Validator
def validate(record)
begin
JSON.parse(record.json_field)
rescue
errors.add(:json_field, 'invalid json')
end
end
end
you wouldn't see invalid string in
record.json_field
only "nil" value, because rails does type casting before passing your value to validator. In order to overcome this, just use
record.json_field_before_type_cast
in your validator.
If you don't fancy enterprise-style validators or monkey-patching the String class here's a simple solution:
class Model < ApplicationRecord
validate :json_field_format
def parsed_json_field
JSON.parse(json_field)
end
private
def json_field_format
return if json_field.blank?
begin
parsed_json_field
rescue JSON::ParserError => e
errors[:json_field] << "is not valid JSON"
end
end
end
Using JSON parser, pure JSON format validation is possible. ActiveSupport::JSON.decode(value) validates value "123" and 123 to true. That is not correct!
# Usage in your model:
# validates :json_attribute, presence: true, json: true
#
# To have a detailed error use something like:
# validates :json_attribute, presence: true, json: {message: :some_i18n_key}
# In your yaml use:
# some_i18n_key: "detailed exception message: %{exception_message}"
class JsonValidator < ActiveModel::EachValidator
def initialize(options)
options.reverse_merge!(message: :invalid)
super(options)
end
def validate_each(record, attribute, value)
if value.is_a?(Hash) || value.is_a?(Array)
value = value.to_json
elsif value.is_a?(String)
value = value.strip
end
JSON.parse(value)
rescue JSON::ParserError, TypeError => exception
record.errors.add(attribute, options[:message], exception_message: exception.message)
end
end
The most simple and elegant way, imo. The top upvoted answers will either return true when passing a string containing integers or floats, or throw an error in this case.
def valid_json?(string)
hash = Oj.load(string)
hash.is_a?(Hash) || hash.is_a?(Array)
rescue Oj::ParseError
false
end
Related
With a validation on a particular attribute using validates, one can, using the :message option, specify the key to the i18n message to be used when validation fails:
In model:
class Foo < ApplicationRecord
validates :some_attribute, ...., message: :bar
end
In locales:
en:
activerecord:
errors:
models:
foo:
attributes:
some_attribute:
bar: "Blah blah"
How can I do the corresponding thing with a validation that (is not specific to a single attribute and) uses the validate method instead of validates?
Using validate with a block
class Foo < ApplicationRecord
validate do
if ERROR_CONDITION
errors.add(:some_attribute, :bar)
end
end
end
OR using validate with a method
class Foo < ApplicationRecord
validate :some_custom_validation_name
private
def some_custom_validation_name
if ERROR_CONDITION
errors.add(:some_attribute, :bar)
end
end
end
errors.add can be used like the following:
errors.add(ATTRIBUTE_NAME, SYMBOL)
SYMBOL corresponds to the message name. See the message column here in this table
i.e. this can be :blank, :taken, :invalid, or :bar (your custom message name)
errors.add(:some_attribute, :taken)
# => ["has already been taken"]
errors.add(:some_attribute, :invalid)
# => ["has already been taken", "is invalid"]
errors.add(:some_attribute, :bar)
# => ["has already been taken", "is invalid", "Blah blah"]
errors.add(ATTRIBUTE_NAME, SYMBOL, HASH)
same as above except that you can also pass in arguments to the message. See the interpolation column here in the same table that you'll need to use.
errors.add(:some_attribute, :too_short, count: 3)
# => ["is too short (minimum is 3 characters)"]
errors.add(:some_attribute, :confirmation, attribute: self.class.human_attribute_name(:first_name))
# => ["is too short (minimum is 3 characters)", "doesn't match First name"]
errors.add(ATTRIBUTE_NAME, STRING)
or pass a custom message string
errors.add(:some_attribute, 'is a bad value')
# => ["is a bad value"]
Also, assuming that you intend to pass in :bar as argument to your validation like in your example, then you can use a custom validator:
# app/validators/lorem_ipsum_validator.rb
class LoremIpsumValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if ERROR_CONDITION
record.errors.add(attribute, options[:message])
end
end
end
# app/models/foo.rb
class Foo < ApplicationRecord
validates :some_attribute, lorem_impsum: { message: :bar }
# or multiple attributes:
validates [:first_name, :last_name], lorem_impsum: { message: :bar }
# you can also combine the custom validator with any other regular Rails validator like the following
# validates :some_attribute,
# lorem_impsum: { message: :bar },
# presence: true,
# length: { minimum: 6 }
end
It looks like it is impossible to do that way. I have to manually add the error message in the validation method.
In my Rails app I want to validate the filter and post_type params.
Both are optional, but if they are present they must have a value and must have a value that matches one in an array of valid values.
In my controller I have two methods for checking them:
def validate_filter
if params.has_key?(:filter)
if params[:filter].present?
if ['popular', 'following', 'picks', 'promoted', 'first-posts'].include?(params[:filter])
return true
else
return false
end
else
return false
end
else
return true
end
end
def validate_post_type
if params.has_key?(:post_type)
if params[:post_type].present?
if ['discussions', 'snaps', 'code', 'links'].include?(params[:post_type])
return true
else
return false
end
else
return false
end
else
return true
end
end
And then in my main controller method I do:
def index
raise ActionController::RoutingError.new('Not Found') unless validate_filter && validate_post_type
...
So this means post_type= and post_type=cam would return a 404 but post_type=snaps would return true.
Is there a better way to validate that the params passed are valid but for both empty and if the key itself exists. Using just blank? and present? is not enough in this scenario.
I would probably move this logic to the model, but if you really want it in the controller you could simplify it.
def validate_filer
return true unless params.has_key?(:filter)
['popular', 'following', 'picks', 'promoted', 'first-posts'].include?(params[:filter])
end
In a case of an API I would consider letting the client know, that there is a validation error rather than just saying 404.
How about using ActiveModel::Validations?
class MyParamsValidator
include ActiveModel::Validations
AVAILABLE_FILTERS = %w(popular following picks promoted first-posts)
# this might come from an enum like MyModel.post_types
AVAILABLE_POST_TYPES = %w(discussions snaps code links)
attr_reader :data
validates :filter, inclusion: { in: AVAILABLE_FILTERS }, allow_blank: true
validates :post_type, inclusion: { in: AVAILABLE_POST_TYPES }, allow_blank: true
def initialize(data)
#data = data
end
def read_attribute_for_validation(key)
data[key]
end
end
class MyController < ApplicationController
before_action :validate_params, only: :index
def validate_params
validator = MyParamsValidator.new(params)
return if validator.valid?
render json: { errors: validator.errors }, status: 422
end
end
You can find more info about a nested case with some tests here.
You can do this in before_action callback of the controller.
before_action :validate_params, only: [:index]
def validate_params
return false unless params[:filter].present? && params[:post_type].present?
params_include?(:filter, %w(popular following picks promoted first-posts))
params_include?(:post_type, %w(discussions snaps code links))
end
Perhaps a small helper method:
def validate_filter
params_include?(:filter, %w(popular following picks promoted first-posts))
end
def validate_filter
params_include?(:post_type, %w(discussions snaps code links))
end
def params_include?(key, values)
!params.key?(key) || values.include?(params[key])
end
It is not clear from your question where that params are coming from, if they are query parameters or part of the path. If they are part of the path you might consider using routing constraints in your routes.rb
I'd like to make full use of the organic character of a NoSQL document and build a dynamic data model which can grow, be changed, and is different for most datasets. Below is the model SomeRequest.rb with the code to set and get from Couchbase, but I can't get the function addOrUpdate(key, value) to work:
undefined method `each' for "0":String
Completed 500 Internal Server
Error in 16ms NoMethodError (undefined method `each' for "0":String):
config/initializers/quiet_assets.rb:7:in `call_with_quiet_assets'
Is the returning error. Is there a way to make this work, to add (or update existing) keys and save the document to the database afterwards?
class SomeRequest < Couchbase::Model
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Callbacks
extend ActiveModel::Naming
# Couch Model
define_model_callbacks :save
attribute :session_id
attribute :views, :default => 0
attribute :created_at, :default => lambda { Time.zone.now }
# iterate through attr keys and set instance vars
def initialize(attr = {})
#errors = ActiveModel::Errors.new(self)
unless attr.nil?
attr.each do |name, value|
setter = "#{name}="
next unless respond_to?(setter)
send(setter, value)
end
end
end
def addOrUpdate(key, value)
self[key] = value
end
def save
return false unless valid?
run_callbacks :save do
Couch.client.set(self.session_id, self)
end
true
end
def self.find(key)
return nil unless key
begin
doc = Couch.client.get(key)
self.new(doc)
rescue Couchbase::Error::NotFound => e
nil
end
end
end
Why don't you like to use find, save and create methods from couchbase-model gem?
class Couchbase::Error::RecordInvalid < Couchbase::Error::Base
attr_reader :record
def initialize(record)
#record = record
errors = #record.errors.full_messages.join(", ")
super("Record Invalid: #{errors}")
end
end
class SomeRequest < Couchbase::Model
include ActiveModel::Validations
attribute :session_id
attribute :views, :default => 0
attribute :created_at, :default => lambda { Time.zone.now }
validates_presence_of :session_id
before_save do |doc|
if doc.valid?
doc
else
raise Couchbase::Error::RecordInvalid.new(doc)
end
end
def initialize(*args)
#errors = ActiveModel::Errors.new(self)
super
end
end
And you might be right, it worth to add validation hooks by default, I think I will do it in next release. The example above is valid for release 0.3.0
What considering updateOrAdd I recommend you just use method #save and it will check if the key is persisted (currently by checking id attribute) and if the record doesn't have key yet, it will generate key and update it.
Update
In version 0.4.0 I added validation hooks into the gem, so the example above could be rewritten simpler.
class SomeRequest < Couchbase::Model
attribute :session_id
attribute :views, :default => 0
attribute :created_at, :default => lambda { Time.zone.now }
validates_presence_of :session_id
end
I have this model:
class Campaign
include Mongoid::Document
include Mongoid::Timestamps
field :name, :type => String
field :subdomain, :type => String
field :intro, :type => String
field :body, :type => String
field :emails, :type => Array
end
Now I want to validate that each email in the emails array is formatted correctly. I read the Mongoid and ActiveModel::Validations documentation but I didn't find how to do this.
Can you show me a pointer?
You can define custom ArrayValidator. Place following in app/validators/array_validator.rb:
class ArrayValidator < ActiveModel::EachValidator
def validate_each(record, attribute, values)
Array(values).each do |value|
options.each do |key, args|
validator_options = { attributes: attribute }
validator_options.merge!(args) if args.is_a?(Hash)
next if value.nil? && validator_options[:allow_nil]
next if value.blank? && validator_options[:allow_blank]
validator_class_name = "#{key.to_s.camelize}Validator"
validator_class = begin
validator_class_name.constantize
rescue NameError
"ActiveModel::Validations::#{validator_class_name}".constantize
end
validator = validator_class.new(validator_options)
validator.validate_each(record, attribute, value)
end
end
end
end
You can use it like this in your models:
class User
include Mongoid::Document
field :tags, Array
validates :tags, array: { presence: true, inclusion: { in: %w{ ruby rails } }
end
It will validate each element from the array against every validator specified within array hash.
Milovan's answer got an upvote from me but the implementation has a few problems:
Flattening nested arrays changes behavior and hides invalid values.
nil field values are treated as [nil], which doesn't seem right.
The provided example, with presence: true will generate a NotImplementedError error because PresenceValidator does not implement validate_each.
Instantiating a new validator instance for every value in the array on every validation is rather inefficient.
The generated error messages do not show why element of the array is invalid, which creates a poor user experience.
Here is an updated enumerable and array validator that addresses all these issues. The code is included below for convenience.
# Validates the values of an Enumerable with other validators.
# Generates error messages that include the index and value of
# invalid elements.
#
# Example:
#
# validates :values, enum: { presence: true, inclusion: { in: %w{ big small } } }
#
class EnumValidator < ActiveModel::EachValidator
def initialize(options)
super
#validators = options.map do |(key, args)|
create_validator(key, args)
end
end
def validate_each(record, attribute, values)
helper = Helper.new(#validators, record, attribute)
Array.wrap(values).each do |value|
helper.validate(value)
end
end
private
class Helper
def initialize(validators, record, attribute)
#validators = validators
#record = record
#attribute = attribute
#count = -1
end
def validate(value)
#count += 1
#validators.each do |validator|
next if value.nil? && validator.options[:allow_nil]
next if value.blank? && validator.options[:allow_blank]
validate_with(validator, value)
end
end
def validate_with(validator, value)
before_errors = error_count
run_validator(validator, value)
if error_count > before_errors
prefix = "element #{#count} (#{value}) "
(before_errors...error_count).each do |pos|
error_messages[pos] = prefix + error_messages[pos]
end
end
end
def run_validator(validator, value)
validator.validate_each(#record, #attribute, value)
rescue NotImplementedError
validator.validate(#record)
end
def error_messages
#record.errors.messages[#attribute]
end
def error_count
error_messages ? error_messages.length : 0
end
end
def create_validator(key, args)
opts = {attributes: attributes}
opts.merge!(args) if args.kind_of?(Hash)
validator_class(key).new(opts).tap do |validator|
validator.check_validity!
end
end
def validator_class(key)
validator_class_name = "#{key.to_s.camelize}Validator"
validator_class_name.constantize
rescue NameError
"ActiveModel::Validations::#{validator_class_name}".constantize
end
end
You'll probably want to define your own custom validator for the emails field.
So you'll add after your class definition,
validate :validate_emails
def validate_emails
invalid_emails = self.emails.map{ |email| email.match(/^([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) }.select{ |e| e != nil }
errors.add(:emails, 'invalid email address') unless invalid_emails.empty?
end
The regex itself may not be perfect, but this is the basic idea. You can check out the rails guide as follows:
http://guides.rubyonrails.org/v2.3.8/activerecord_validations_callbacks.html#creating-custom-validation-methods
Found myself trying to solve this problem just now. I've modified Tim O's answer slightly to come up with the following, which provides cleaner output and more information to the errors object that you can then display to the user in the view.
validate :validate_emails
def validate_emails
emails.each do |email|
unless email.match(/^([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})$/i)
errors.add(:emails, "#{email} is not a valid email address.")
end
end
end
Here's an example that might help out of the rails api docs: http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates
The power of the validates method comes when using custom validators and default validators in one call for a given attribute e.g.
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors[attribute] << (options[:message] || "is not an email") unless
value =~ /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
end
end
class Person
include ActiveModel::Validations
attr_accessor :name, :email
validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 }
validates :email, :presence => true, :email => true
end
I want to have an ability to set a custom message in a model validator method to notify a user about incorrect input data.
Firstly, I set a custom validator class where I redefine the validate_each method in that way as it recommended in rails' documentation:
# app/models/user.rb
# a custom validator class
class IsNotReservedValidator < ActiveModel::EachValidator
RESERVED = [
'admin',
'superuser'
]
def validate_each(record, attribute, value)
if RESERVED.include? value
record.errors[attribute] <<
# options[:message] assigns a custom notification
options[:message] || 'unfortunately, the name is reserved'
end
end
end
Secondary, I try to pass a custom message to the validates method by two different ways:
# a user model
class User < ActiveRecord::Base
include ActiveModel::Validations
ERRORS = []
begin
validates :name,
:is_not_reserved => true,
# 1st try to set a custom message
:options => { :message => 'sorry, but the name is not valid' }
rescue => e
ERRORS << e
begin
validates :name,
:is_not_reserved => true,
# 2nd try to set a custom message
:message => 'sorry, but the name is not valid'
rescue => e
ERRORS << e
end
ensure
puts ERRORS
end
end
But neither of that methods works:
>> user = User.new(:name => 'Shamaoke')
Unknown validator: 'options'
Unknown validator: 'message'
Where and how can I set custom messages for custom validators?
Thanks.
Debian GNU/Linux 5.0.6;
Ruby 1.9.2;
Ruby on Rails 3.0.0.
First of all, don't include ActiveModel::Validations, it's already included in ActiveRecord::Base. And secondly, you don't specify the options for a validation with an :options key, you do it with the key for your validator.
class User < ActiveRecord::Base
validates :name,
:is_not_reserved => { :message => 'sorry, but the name is not valid' }
end