Skip to content

Specify class when including relationships #75

Open

Description

@KidA001

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.

Activity

  1. changed the title [-]Specify class for include relatoinship[/-] [+]Specify class for include relationship[/+] on Dec 12, 2017
  2. changed the title [-]Specify class for include relationship[/-] [+]Specify class when including relationships[/+] on Dec 12, 2017
  3. beauby commented on Dec 12, 2017

    @beauby
    Member

    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
  4. kapso commented on Apr 20, 2018

    @kapso

    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?

  5. siepet commented on Jul 31, 2018

    @siepet

    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.

  6. beauby commented on Jul 31, 2018

    @beauby
    Member

    @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
  7. beauby commented on Jul 31, 2018

    @beauby
    Member

    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.

  8. kapso commented on Jul 31, 2018

    @kapso

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

  9. Startouf commented on Sep 12, 2018

    @Startouf

    @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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions