jsonapi-rails
jsonapi-rails copied to clipboard
Specify class when including relationships
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.
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
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?
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.
@kapso @siepet If you look at the code history, it used to be that way. The reasons it was modified:
- 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).
- 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
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.
@beauby yea I ended up using a controller method as well in my BaseController
@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)