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

Specify class when including relationships #75

Open
KidA001 opened this issue Dec 12, 2017 · 7 comments
Open

Specify class when including relationships #75

KidA001 opened this issue Dec 12, 2017 · 7 comments

Comments

@KidA001
Copy link

KidA001 commented Dec 12, 2017

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.

@KidA001 KidA001 changed the title Specify class for include relatoinship Specify class for include relationship Dec 12, 2017
@KidA001 KidA001 changed the title Specify class for include relationship Specify class when including relationships Dec 12, 2017
@beauby
Copy link
Member

beauby commented Dec 12, 2017

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

kapso commented Apr 20, 2018

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

siepet commented Jul 31, 2018

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

beauby commented Jul 31, 2018

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

beauby commented Jul 31, 2018

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

kapso commented Jul 31, 2018

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

@Startouf
Copy link

Startouf commented Sep 12, 2018

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

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

No branches or pull requests

5 participants