I had a similar need although the specific need was for sending a relation to an ActiveJob. Sharing it even if that is not your goal as it might be a useful pointer in the right direction or something you can build on.
The key thing for me was limiting the scope of what can be serialized. There is a method called where_values_hash which returns the where conditions hash. The caveat is it is a public method although nodoc so that makes is pseudo-private.
Since it just provides the where hash this means it cannot serialize things like a joins, includes or even where.not. Also SQL fragments won’t work such as where('name ILIKE ?', 'John%'). But it does still work if you have multiple where conditions such as in a method chain that is built up: Widget.where(foo: 'bar').where(baz: 'boo').
Most of the time this is fine and even if not I can usually work around the limitation in some way. Here is the ActiveJob serializer I wrote:
class ActiveJob::RelationSerializer < ActiveJob::Serializers::ObjectSerializer
def serialize? argument
super && where_hash_only?(argument)
end
def serialize scope
super klass: scope.klass, conditions: scope.where_values_hash
end
def deserialize serialized
serialized[:klass].where serialized[:conditions]
end
private
def where_hash_only? scope
scope.to_sql == scope.klass.where(scope.where_values_hash).to_sql
end
def klass = ActiveRecord::Relation
end
Note the check on serialization to see if the scope violates any of my limitations. I take the scope being serialized and then try to re-created it with the where_values_hash. If the SQL string doesn’t match then that means someone tried to serialize a relation that was more complex than this allowed.
If all this is too limiting then my next strategy is just to pluck the ids. Example:
ids = Widget.some_complex_scope.pluck(:id)
Then later when I need the records in that scope I can:
Widget.where id: ids
The caveat here is the records in that set may change between the time it was seralized and the time it was deserialized. Also anything like a pre-load (includes, etc) won’t happen since those are secondary queries.