jsonapi-rb / jsonapi-rails

Rails gem for fast jsonapi-compliant APIs.
http://jsonapi-rb.org
MIT License
319 stars 63 forks source link

Specify class when including relationships #75

Open KidA001 opened 6 years ago

KidA001 commented 6 years ago

In my controller I have

module API
  module V1
    class ProjectsController < API::V1::ApplicationController

      # GET /projects
      def index
        render jsonapi: Project.all,
               class: { Project: API::V1::SerializableProject },
               include: params[:include]
      end

In my Serializer I have

module API
  module V1
    class SerializableProject < JSONAPI::Serializable::Resource
      type 'projects'

      attributes :name, :job_number

      has_many :companies
    end
  end
end

When I pass the include params with GET /projects?include=companies, I get undefined method 'new' for nil:NilClass. I'm assuming because it can't find API::V1::SerializableCompany

How am I supposed to specify the class for all includes? This was a simplified example, but some of my Models have multiple relationships.

beauby commented 6 years ago

Hi @KidA001 – the class render option takes a hash that maps your models to your serializers. In your case, you should do:

render jsonapi: Project.all,
               class: { Project: API::V1::SerializableProject, Company: API::V1::SerializableCompany },
               include: params[:include]

If your serializers have a consistent naming scheme, you could override the jsonapi_class hook as follows:

module API
  module V1
    class ProjectsController < API::V1::ApplicationController
      def jsonapi_class
        Hash.new { |h, k| h[k] = "API::V1::Serializable#{k}".safe_constantize }
      end

      # GET /projects
      def index
        render jsonapi: Project.all,
               include: params[:include]
      end
kapso commented 6 years ago

I think a cleaner approach would be to specify the class name inside serializer, so something like

belongs_to :customer, class: Api::V1::SerializableCustomer

That keeps the render method clean, specially if you many nested objects -- in which case you could end up with a big hash. This apprach is similar to AMS.

@beauby thoughts?

siepet commented 6 years ago

I agree with @kapso, going with default serializer class for relationship by doing

belongs_to :customer, class: Api::V1::SerializableCustomer

would be a nice thing to add.

beauby commented 6 years ago

@kapso @siepet If you look at the code history, it used to be that way. The reasons it was modified:

  1. It introduces possible inconsistencies (say you're requesting a post and their comments, and the authors of those – a same author could be serialized by two possibly different serializers).
  2. It makes debugging harder because it is not clear what serializer was used and how it was chosen.

Note that in most use-cases, you would use the controller-level hooks to provide a static or dynamic hash, i.e.

def jsonapi_class
  @jsonapi_classes ||= { 
    # ...
  }
end
beauby commented 6 years ago

And as mentioned, if you have a consistent way of deriving the serializer name from the class name (which you should probably have), you can use a dynamic hash to lazily generate the mapping.

kapso commented 6 years ago

@beauby yea I ended up using a controller method as well in my BaseController

Startouf commented 5 years ago

@beauby

It introduces possible inconsistencies (say you're requesting a post and their comments, and the authors of those – a same author could be serialized by two possibly different serializers)

My code relied exactly on what you called "inconsistencies" to produce tweaks in the serialization that best fit my needs

I have an appointment booking website where users can only access the phone number of people they have an appointments with. Assume I am rendering a user with his contacts, and I want to unlock the phone number for the users they are in contact with through appointments

class User
  has_many :conversations
  has_many :appointments, through: :conversations
  has_and_belongs_to_many :contacts, class_name: :User
  field :phone
end

class Conversation
  belongs_to :initiator, class_name: User
  belongs_to :recipient, class_name: User
  has_one :appointment
end

class Appointment
  belongs_to :conversation
end

I was taking advantage of a specific "serializer routing" user.appointments.conversation.recipient VS user.contacts to show the phone number

user = create(:user)
user_whose_phone_will_be_shared_by_appointment = create(:user)
conversation_with_user = create(:conversation, initiator: user, recipient: user_whose_phone_will_be_shared_by_appointment)
appointment_in_conversation = create(:appointment, conversation: conversation)

render(
  jsonapi: user,
  includes: [
    appointments: [
      conversation: [ 
        initiator: [], recipient: []
      ]
    ],
    contacts: []
)

The phone number would be serialized for those users that have an appointment with the user, since I would declare a different serializer in the ConversationSerializer

class AppointmentSerializer
  belongs_to :conversation, class: Appointment::ConversationSerializer
end

class Appointment::ConversationSerializer
  belongs_to :recipient, class:: Appointment::Conversation::UserWithPhoneSerializer
  belongs_to :initiator, class:: Appointment::Conversation::UserWithPhoneSerializer
end

class Appointment::Conversation::UserWithPhoneSerializer
  attribute :phone
end

# VS 

class UserSerializer
  # no phone attribute
  has_many :contacts, class_name: 'UserSerializer'
end

Now I agree this is a bit of a (dirty hack), since we have no way of deciding which serializer to actually use (not sure where this is a first hit first serialize or something else), but it did serve me perfectly on this one. Not sure how to reproduce something similar on the new jsonapi-rb 0.3+. Or maybe something using decorators ? sounds like painful...

But if you have an idea / suggestion and it works, I'd migrate right away to jsonapi-rails 3.x

This is basically what prevent me from upgrading to jsonapi_compliable 0.11 of @richmolj that upgraded jsonapi-rails 0.2.x to 0.3.x

It makes debugging harder because it is not clear what serializer was used and how it was chosen.


class ApplicationSerializer < JSONAPI::Serializable::Resource

if Rails.env.development?

@override in dev environment to inject serializer name

def as_jsonapi(*)
  super.tap do |hash|
    (hash[:meta] ||= {}).merge!(serializer_name: self.class.name)
  end
end

end end


killed it once and for all (maybe not for the "how it was chosen" part, but this is mainly the library user's job I would say)