Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[0.10.0rc1] Odd results from #to_json #878

Closed
JustinAiken opened this issue Apr 22, 2015 · 9 comments
Closed

[0.10.0rc1] Odd results from #to_json #878

JustinAiken opened this issue Apr 22, 2015 · 9 comments

Comments

@JustinAiken
Copy link
Contributor

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
Copy link

👍 same here :(

@nfm
Copy link

nfm commented Apr 23, 2015

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
Copy link
Contributor Author

Hmm, so either:

  • to_json should never be called on a serializer, and I should change my code to upgrade
  • ActiveModel::Serializer should have a #to_json method something like:
def to_json
  self.class.adapter.new(self).to_json
end

?

@joaomdmoura
Copy link
Member

@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
Copy link
Contributor Author

Happy to submit it as a PR w/ tests if you you do want it 👍

@bf4
Copy link
Member

bf4 commented Apr 24, 2015

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
Copy link
Contributor

beauby commented Sep 24, 2015

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
Copy link

@beauby How to pass fields argument to it?

@rails-api rails-api locked and limited conversation to collaborators Mar 2, 2016
@bf4
Copy link
Member

bf4 commented Mar 2, 2016

@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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

7 participants