Search code examples
ruby-on-railsruby-on-rails-3cancan

Cancan: Ability class to json


I have Rails 3 application with Cancan 1.6.9 for authorization. I want to convert Ability class to JSON. Here is the my Ability class.

# encoding: utf-8

module Ability

  def self.for(guest)
    guest ||= User.new # guest user (not logged in)

    if guest.admin?
      AdminAbility.new(guest)
    elsif guest.assurer?
      AssurerAbility.new(guest)
    end
  end

  ##
  # Assurer
  class AssurerAbility
    include CanCan::Ability

    def initialize(user)
      cannot :manage, User
      can :read, ExchangeRate
    end
  end

  ##
  # Admin
  class AdminAbility
    include CanCan::Ability

    def initialize(user)
      can :manage, User
      can :manage, ExchangeRate
    end

  end

end

When I run Ability.for(User.last).to_json I get following errors:

ActiveSupport::JSON::Encoding::CircularReferenceError: object references itself
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:75:in `check_for_circular_references'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:46:in `encode'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `block in encode_json'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `each'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `map'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `encode_json'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:48:in `block in encode'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:77:in `check_for_circular_references'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:46:in `encode'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `block in encode_json'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `each'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `map'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `encode_json'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:48:in `block in encode'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:77:in `check_for_circular_references'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:46:in `encode'
... 19 levels...
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:46:in `encode'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `block in encode_json'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `each'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `map'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `encode_json'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:48:in `block in encode'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:77:in `check_for_circular_references'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:46:in `encode'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:31:in `encode'
    from /home/user/.rvm/gems/[email protected]/gems/activesupport-3.2.13/lib/active_support/core_ext/object/to_json.rb:16:in `to_json'
    enter code here

And when I run Ability.for(User.last).as_json I get result. But it's not valid JSON result.

{"rules"=>[#<CanCan::Rule:0xaf5e81c @match_all=false, @base_behavior=false, @actions=[:manage], @subjects=[User(id: integer, password_digest: string, created_at: datetime, updated_at: datetime, uic: string, role: string, name: string, register_no: string)], @conditions={}, @block=nil>, #<CanCan::Rule:0xaf5e72c @match_all=false, @base_behavior=true, @actions=[:read], @subjects=[ExchangeRate(id: integer, data: float, date: date, created_at: datetime, updated_at: datetime)], @conditions={}, @block=nil>]}

Any idea?


Solution

  • TL;DR: Implement self.as_json method in your models and explicitly specify which fields to convert to JSON. After this you will be able to run Ability.for(User.last).to_json without errors.


    Complete explanation: let's look at the structure of Ability as seen from source:

    • Ability instance has a field named @rules.
    • Each rule has a field named @subject which is a flattened ActiveRecord class (User, ExchangeRate etc). Yes, class, not class instance.
    • @subject gets converted to JSON and CircularReferenceError appears. Why?

    The problem is, converting User or ExchangeRate class to JSON is not a good idea because their structure may be very complicated. For instance, if you try running User.as_json, the result might be like this. Probably one of User's fields references back to User and you get CircularReferenceError. To avoid this, specify which fields should be included into JSON representation of User (the same is applied for ExchangeRate):

    class User < ActiveRecord::Base
      #...
      def self.as_json(options = {})
        { "class" => "User" }
      end
    end