1

I want to store an array of objects in JSONB column in PostgreSQL. I'm using Rails 5.2. I'm using a custom serializer that ensures the value assigned to a JSONB field is an Array not a Hash. And I'm getting an error when assigning something like [{a: 1}] to the field. Here is the code:
model:

class Printing
  serialize :card_faces, CardFacesSerializer
end

serializer:

class CardFacesSerializer
  include JSONBArraySerializer

  def allowed_attributes
    %i[name image]
  end
end

serializer concern:

module JSONBArraySerializer
  extend ActiveSupport::Concern

  def initialize(data)
    return [] if data.blank?

    if data.is_a?(String)
      json = Oj.load(data, symbol_keys: true)
    end
    raise ArgumentError, "#{json} must be [{},{}], not {}" if json.is_a?(Hash)
    # Will only set the properties that are allowed
    json.map do |hash|
      hash.slice(self.allowed_attributes)
    end
  end

  class_methods do
    def load(json)
      return [] if json.blank?
      self.new(json)
    end

    def dump(obj)
      # Make sure the type is right.
      if obj.is_a?(self)
        obj.to_json
      else
       raise StandardError, "Expected #{self}, got #{obj.class}"
      end
    end
  end
end

When evaluating:

pr = Printing.first
pr.card_faces = [{hay: 12}]
pr.save!

I get an error:

StandardError: Expected CardFacesSerializer, got Array

I don't think it's clear for me how dump/load work. Why dump is being called during save? How can I fix my code to work properly?

UPDATE
I managed to make it work with this code of serializer concern:

module JSONBArraySerializer
  extend ActiveSupport::Concern

  class_methods do
    def load(data)
      return [] if data.blank?

      if data.is_a?(String)
        json = Oj.load(data, symbol_keys: true)
      end
      raise ArgumentError, "#{json} must be [{},{}], not {}" if json.is_a?(Hash)

      # Will only set the properties that are allowed
      json.map do |hash|
        hash.slice(*allowed_attributes)
      end
    end

    def dump(obj)
      # Make sure the type is right.
      if obj.is_a?(Array)
        obj.to_json
      else
       raise ArgumentError, "Expected Array, got #{obj.class}"
      end
    end
  end
end
6
  • I believe, load is called when AR object is instantiating and dump when saved. So dump should await for Array (desired column value) not self (Serializer). Commented Jan 10, 2019 at 8:47
  • Also, I think serialization is not needed here. When you have JSONB column, you can just check column value by custom validation. api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/… Commented Jan 10, 2019 at 8:49
  • Out of curiosity, if you're storing an array, why use a JSONB column and not an array column? Also, why use a concern and why not include the concern code in your serializer? Commented Jan 10, 2019 at 9:08
  • @jvillian because I want to store an array of objects as you can see in my code [{},{}] and I don't want to serialize objects into strings as it's costly and it won't be searchable and indexable. Commented Jan 10, 2019 at 10:13
  • You can also use gem activerecord_json_validator (or underlying gem json-schema) for extended validation of your JSONB column. Commented Jan 10, 2019 at 10:33

1 Answer 1

5

Don't use serialize with JSON/JSONB columns.

Keep in mind that database adapters handle certain serialization tasks for you. For instance: json and jsonb types in PostgreSQL will be converted between JSON object/array syntax and Ruby Hash or Array objects transparently. There is no need to use serialize in this case. https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html

serialize is an old hack used to store JSON/YAML/whatever in string columns. Seriously - don't use it. It will just lead to issues with double conversion.

What you are doing should instead be handled by a regular model validation and / or a custom setter.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the explanation

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.