Jordan
Jordan

Reputation: 411

Filtering fields from ActiveRecord/ActiveModel JSON output (by magic!)

I want to filter out specific fields from ActiveRecord/ActiveModel classes when outputting JSON.

The most straightforward way to do this is just overriding as_json, perhaps like so:

def as_json (options = nil)
  options ||= {}
  super(options.deep_merge({:except => filter_attributes}))
end

def filter_attributes
  [:password_digest, :some_attribute]
end

This works, but it's a little verbose and lends itself to not being DRY pretty fast. I thought it would be nice to just declare the filtered properties with a magical class method. For example:

class User < ActiveRecord::Base
  include FilterJson

  has_secure_password
  filter_json :password_digest
  #...
end

module FilterJson
  extend ActiveSupport::Concern

  module ClassMethods
    def filter_json (*attributes)
      (@filter_attributes ||= Set.new).merge(attributes.map(&:to_s))
    end

    def filter_attributes
      @filter_attributes
    end
  end

  def as_json (options = nil)
    options ||= {}
    super(options.deep_merge({:except => self.class.filter_attributes.to_a}))
  end
end

The problem with this is getting it to deal with inheritance properly. Let's say I subclass User:

class SecretiveUser < User
  filter_json :some_attribute, :another_attribute
  #...
end

Logically, it makes sense to filter out :some_attribute, :another_attribute, and also :password_digest.

However, this will only filter the attributes declared on the class. To the desired end, I tried to call super within filter_attributes, but that failed. I came up with this, and it's a hack.

def filter_attributes
  if superclass.respond_to?(:filter_attributes)
    superclass.filter_attributes + @filter_attributes
  else
    @filter_attributes
  end
end

This is obviously brittle and not idiomatic, but there's the "what" that I'm trying to accomplish. Can anyone think of a way to do it more correctly (and hopefully more elegantly)? Thanks!

Upvotes: 3

Views: 1939

Answers (1)

deefour
deefour

Reputation: 35370

I think it is a safer solution to white-list attributes than to black-list them. This will prevent unwanted future attributes added to User or SomeUser from making it into your JSON response because you forgot to add said attributes to filter_json.

You seem to be looking for a solution to your specific inheritance issue. I'm still going to point out active_model_serializers, as I feel it is a saner way to manage serialization.

class UserSerializer < ActiveModel::Serializer
  attributes :id, :first_name, :last_name
end

class SecretUserSerializer < UserSerializer
  attributes :secret_attribute, :another_attribute
end

Given some SecretUser s you can do

SecretUserSerializer.new(s).as_json

and you'll get :id, :first_name, :last_name, :secret_attribute, and :another_attribute. The inheritance works as expected.

Upvotes: 4

Related Questions