Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Validation of objects inside array of jsonb objects with RubyOnRails

How would you validate each object inside an array of object with Rails?

I am building a user profile form in our Rails app. Inside user model, we have basic string attributes but some jsonb fields as well. JSONb fields default to [] because we want to store an array of objects inside that attribute. Here is an example of simplified user model attributes:

  • name: string
  • email: string
  • education: jsonb, default: []

Education is an array of objects such as:

[{
  school: 'Harvard university',
  degree: 'Computer Science',
  from_date: 'Tue, 11 Jul 2017 16:22:12 +0200`,
  to_date: 'Tue, 11 Jul 2017 16:22:12 +0200'
},{
  school: 'High school',
  degree: 'Whatever',
  from_date: 'Tue, 11 Jul 2017 16:22:12 +0200`,
  to_date: 'Tue, 11 Jul 2017 16:22:12 +0200'
}]

User should be able to click Add school button, to add more fields via jquery. That jquery part is not important for this question - maybe just an explanation why we used an Array of objects.

How would you validate each item in education array, so I can mark the text field containing validtion error with red color? I got adviced that using FormObject pattern might help here. I have also tried writing custom validator that inherits from ActiveModel::Validator class, but the main problem still lies in fact, that I am dealing with an array, not actual object..

Thanks for any constructive help.

like image 837
Lubomir Herko Avatar asked Jul 11 '17 14:07

Lubomir Herko


2 Answers

You could treat education records as first-class citizens in your Rails model layer by introducing a non-database backed ActiveModel model class for them:

class Education
  include ActiveModel::Model

  attr_accessor :school, :degree, :from_date, :to_date

  validates :school, presence: true
  validates :degree, presence: true

  def initialize(**attrs)
    attrs.each do |attr, value|
      send("#{attr}=", value)
    end
  end

  def attributes
    [:school, :degree, :from_date, :to_date].inject({}) do |hash, attr|
      hash[attr] = send(attr)
      hash
    end
  end

  class ArraySerializer
    class << self
      def load(arr)
        arr.map do |item|
          Education.new(item)
        end
      end

      def dump(arr)
        arr.map(&:attributes)
      end
    end
  end
end

Then you can transparently serialize and deserialize the education array in your User model:

class User
   # ...
   serialize :education, Education::ArraySerializer
   # ...
end

This solution should allow you to validate individual attributes of Education objects with built-in Rails validators, embed them in a nested form, and so on.

Important: I wrote the code above without testing it, so it might need a few modifications.

like image 182
Mate Solymosi Avatar answered Nov 18 '22 17:11

Mate Solymosi


Something like this perhaps? (probably you'll want the hash to be passed with keys as strings and not symbols and then verified for item[string] not item[symbol]

class MyModel < ActiveRecord::Base
  include ActiveModel::Validations
  validates :education, evalidation: true

  class EvalidationValidator < ActiveModel::EachValidator
    def validate_each(record, attribute, value)
      record.errors[attribute] << "must be a valid array & json" unless (check_json_array(value) rescue nil)
    end
  end

  def check_json_array(value)
     return false unless value.is_a?(Array)
     value.each do |item|
       return false unless item.is_a?(Hash)
       return false if item[:school].blank? || item[:degree].blank? || !valid_date_time(item[:from_date]) || !valid_date_time(item[:to_date])
     end
     true
  end

  def valid_date_time(date)
     #this needs work as it will pass for instance "Tue" as true, since DateTime will parse even single day initials to the current closest date
    (DateTime.parse(date) rescue nil)
  end
end
like image 36
m3characters Avatar answered Nov 18 '22 18:11

m3characters