rails-api / active_model_serializers

ActiveModel::Serializer implementation and Rails hooks
MIT License
5.32k stars 1.39k forks source link

[0.10.0rc1] Odd results from `#to_json` #878

Closed JustinAiken closed 8 years ago

JustinAiken commented 9 years ago

With this simple serializer:

class ExerciseEquipmentSerializer < ActiveModel::Serializer
  attributes :id, :name
end

and then this test:

require 'spec_helper'

describe ExerciseEquipmentSerializer do
  let(:exercise_equipment)    { create :exercise_equipment }
  let(:serializer)            { ExerciseEquipmentSerializer.new exercise_equipment }
  let(:result)                { JSON.parse(serializer.to_json) }

  it "huh?" do
    expect(result).to eq id: 1, name: 'name'
  end
end

I get this:

expected: {:id=>1, :name=>"name"}
got:      {"object"=>{"id"=>1, "name"=>"name", "description"=>"description", "image"=>{"url"=>nil}, "created_at"=>"2015-04-22T20:52:06.794Z", "updated_at"=>"2015-04-22T20:52:06.794Z"}, "options"=>{}, "root"=>false, "meta"=>nil, "meta_key"=>nil, "scope"=>nil}

       (compared using ==)

       Diff:
       @@ -1,3 +1,7 @@
       -:id => 1,
       -:name => "name",
       +"meta" => nil,
       +"meta_key" => nil,
       +"object" => {"id"=>1, "name"=>"name", "description"=>"description", "image"=>{"url"=>nil}, "created_at"=>"2015-04-22T20:52:06.794Z", "updated_at"=>"2015-04-22T20:52:06.794Z"},
       +"options" => {},
       +"root" => false,
       +"scope" => nil,

which seems a bit odd compared to what I was getting before in 0.8.x...

sebastianvera commented 9 years ago

:+1: same here :(

nfm commented 9 years ago

I think you need to run the serializer through the adapter. I'm not sure if this is the intended way to invoke the adapter, but this should produce the output you're expecting:

let(:result) { JSON.parse(ActiveModel::Serializer.adapter.new(serializer).to_json) }
JustinAiken commented 9 years ago

Hmm, so either:

def to_json
  self.class.adapter.new(self).to_json
end

?

joaomdmoura commented 9 years ago

@nfm is right, this is how we have being testing it internally:

adapter = ActiveModel::Serializer.adapter.new(serializer)
adapter.to_json

But agree with @JustinAiken, would be nice to have thins implementation inside the serializer. Any thoughts @kurko?

JustinAiken commented 9 years ago

Happy to submit it as a PR w/ tests if you you do want it :+1:

bf4 commented 9 years ago

Yeah, would be nice.

FWIW, I've written this spec helper code

begin
  require "active_model_serializers"
  module SerializerSupport
    def serialize(resource)
      adapter = serialization_adapter_for(resource)
      yield adapter if block_given?
      adapter.as_json
    end

    def serialization_adapter_for(resource)
      serializer = serializer_for(resource)
      # SERIALIZER_OPTS = :serializer, :each_serializer, etc
      serializer_opts = { each_serializer: serializer }
      object = serializer.new(resource, serializer_opts)
      # ADAPTER_OPTION_KEYS = [:include, :fields, :root, :adapter]
      adapter_opts = { root: resource_root(resource) }
      ActiveModel::Serializer::Adapter.create(object, adapter_opts)
    end

    def resource_root(resource)
      if resource.respond_to?(:first)
        fail "Resource can't be empty #{resource}" if resource.empty?
        resource.first.class.table_name
      else
        resource.class.table_name
      end
    end

    def serializer_for(resource)
      ActiveModel::Serializer.serializer_for(resource)
    end

    def serializer_attributes(serializer)
      serializer._attributes
    end

    def serializer_associations(serializer)
      serializer._associations
    end

    def serializer_association_names(serializer)
      serializer_associations(serializer).keys
    end

    def serialized_resource_attributes(resource)
      serialization_adapter_for(resource).serializable_hash
    end
  end
  with_serializer_support = {
    type: ->(v) { [:serializer, :controller].include?(v) }
  }
  RSpec.configure do |config|
    config.include SerializerSupport, with_serializer_support
  end
rescue LoadError
end

and this shared helper

# Usage:
#
# RSpec.describe UserSerializer, type: :serializer do
#   it_behaves_like "a record serializer" do
#     let(:attributes) { %i[id created_at] }
#     let(:record) { create(:user, posts: [build(:post)]) }
#   end
# end
#
# Note that the record will be written out as an
# example serialized resource, so you'll want to ensure
# you set attributes that don't change from run to run.
#
# Depends on spec/support/serializers.rb
RSpec.shared_examples "a record serializer" do
  let(:serializer)       { serializer_for(record) }
  let(:resource_name)    { record.class.name.underscore }
  let(:nested_resources) { serializer_association_names(serializer) }
  let(:serialized_attributes) do
    serialized_resource_attributes(record).keys
  end

  it "with attributes" do
    serialized_keys = attributes.dup
    expect(serializer_attributes(serializer)).to match_array(serialized_keys)
  end

  it "serializes a record" do
    serialized_resource = serialize(record)

    serialized_keys = attributes.dup
    serialized_keys += nested_resources unless nested_resources.empty?

    expect(serialized_keys).to match_array(serialized_resource.keys)

    writeout_resource(resource_name, serialized_resource)
  end

  it "serializes a record's nested resources" do
    serialized_resource = serialize(record)
    nested_resources.each do |nested_root|
      # the nested resource isn't an attribute, so we have to call a method
      # to get it. record[nested_root] doesn't work
      nested_resource = record.public_send(nested_root)
      called = false
      # Get the adapter
      # so we can get the serializer
      # so we can get only the attributes of the nested resource
      # and thereby exclude associations aren't serialized in a nested resource
      serialize(nested_resource) do |adapter|
        serializer = adapter.serializer

        if serializer.respond_to?(:each)
          serialized_nested_resource = serializer.map(&:attributes)
        else
          serialized_nested_resource = serializer.attributes
        end

        expect(serialized_resource[nested_root]).to eq(serialized_nested_resource)

        called = true
      end

      # sanity check
      expect(called).to eq(true)
    end
  end
end

And I've written some other code to mimic the controller action _render_with_renderer_json (called by _render_to_body_with_renderer)

opts = {
status: 200,
prefixes: %w[items application],
template: params["action"],
scope: nil,
scope_name: _serialization_scope
}
serializer_instance = ActiveModel::Serializer::ArraySerializer.new(resource, opts)
# or
serializer_instance = ItemSerializer.new(resource, opts)
opts = {root: "items"}
adapter = ActiveModel::Serializer::Adapter.create(serializer_instance, opts)
adapter.as_json
# https://github.com/rails-api/active_model_serializers/blob/master/lib/action_controller/serialization.rb
# https://github.com/rails-api/active_model_serializers/blob/1577969cb763/test/serializers/meta_test.rb
# https://github.com/rails-api/active_model_serializers/issues/870
# https://github.com/rails-api/active_model_serializers/issues/876

And use as_json in my json renderer

# In dev/test pretty print JSON
# ref
# https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_controller/metal/renderers.rb#L66-L128
# https://github.com/rails/rails/blob/4-2-stable//actionview/lib/action_view/renderer/renderer.rb#L32
#
# Consider instead using: https://github.com/rack/rack/blob/master/lib/rack/deflater.rb
# or see
# https://github.com/brianhempel/stream_json_demo/commit/6bd580ea9bf3b1d508bb1ce9e48834bf67e313df
# http://collectiveidea.com/blog/archives/2015/03/13/optimizing-rails-for-memory-usage-part-4-lazy-json-generation-and-final-thoughts/
if Rails.env.development? || Rails.env.test?
  gem "rails", "~> 4.2"
  ActionController::Renderers.remove :json
  ActionController::Renderers.add :json do |json, options|
    if !json.is_a?(String)
      # changed from
      # json = json.to_json(options)
      # changed to
      json = json.as_json(options) if json.respond_to?(:as_json)
      json = JSON.pretty_generate(json, options)
    end

    if options[:callback].present?
      if content_type.nil? || content_type == Mime::JSON
        self.content_type = Mime::JS
      end

      "/**/#{options[:callback]}(#{json})"
    else
      self.content_type ||= Mime::JSON
      json
    end
  end
end
beauby commented 8 years ago

So the current way (in master) to "get the JSON" is to do ActiveModel::SerializableResource.new(my_resource).serializable_hash.to_json.

Closing this for now. Feel free to reopen if needed, or open a new issue with a feature request if you feel the interface should be different.

williamweckl commented 8 years ago

@beauby How to pass fields argument to it?

bf4 commented 8 years ago

@williamweckl see https://github.com/rails-api/active_model_serializers/blob/v0.10.0.rc4/test/adapter/json_api/fields_test.rb#L50-L51

render @post, adapter: :json_api, fields: { posts: [:author] }

PR for docs https://github.com/rails-api/active_model_serializers/blob/master/docs/general/rendering.md#fields appreciated!

If this didn't answer your question, please open a new issue.