Search code examples
ruby-on-railsarraysvalidationjsonb

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.


Solution

  • 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.