jsonapi-serializer hide the id of a model - ruby-on-rails

Using jsonapi-serializer I don't want to return the ID of the blog post, eventually I'll return a custom slug instead, but I cant figure out how to remove fields in the serializer.
{
"data": {
"id": "6",
"type": "blog_post",
"attributes": {
"email": "mark#example.com",
"name": "Mark"
}
}
}
I tried using a conditional proc to hide the id to no avail:
class BlogPostSerializer
include JSONAPI::Serializer
attributes :email, :name
attribute :id, if: Proc.new { false }
end

Ah it turns out you need an ID per the JSON API spec:
https://discuss.jsonapi.org
I didn't want to reveal the actual id of the blog post however, so you can tell the serializer to return the uuid instead:
set_id :uuid
attributes :email, :name, :uuid

Related

How to save a nested many-to-many relationship in API-only Rails?

In my Rails (api only) learning project, I have 2 models, Group and Artist, that have a many-to-many relationship with a joining model, Role, that has additional information about the relationship. I have been able to save m2m relationships before by saving the joining model by itself, but here I am trying to save the relationship as a nested relationship. I'm using the jsonapi-serializer gem, but not married to it nor am I tied to the JSON api spec. Getting this to work is more important than following best practice.
With this setup, I'm getting a 500 error when trying to save with the following errors:
Unpermitted parameters: :artists, :albums and ActiveModel::UnknownAttributeError (unknown attribute 'relationships' for Group.)
I'm suspecting that my problem lies in the strong param and/or the json payload.
Models
class Group < ApplicationRecord
has_many :roles
has_many :artists, through: :roles
accepts_nested_attributes_for :artists, :roles
end
class Artist < ApplicationRecord
has_many :groups, through: :roles
end
class Role < ApplicationRecord
belongs_to :artist
belongs_to :group
end
Controller#create
def create
group = Group.new(group_params)
if group.save
render json: GroupSerializer.new(group).serializable_hash
else
render json: { error: group.errors.messages }, status: 422
end
end
Controller#group_params
def group_params
params.require(:data)
.permit(attributes: [:name, :notes],
relationships: [:artists])
end
Serializers
class GroupSerializer
include JSONAPI::Serializer
attributes :name, :notes
has_many :artists
has_many :roles
end
class ArtistSerializer
include JSONAPI::Serializer
attributes :first_name, :last_name, :notes
end
class RoleSerializer
include JSONAPI::Serializer
attributes :artist_id, :group_id, :instruments
end
Example JSON payload
{
"data": {
"attributes": {
"name": "Pink Floyd",
"notes": "",
},
"relationships": {
"artists": [{ type: "artist", "id": 3445 }, { type: "artist", "id": 3447 }]
}
}
Additional Info
It might help to know that I was able to save another model with the following combination of json and strong params.
# Example JSON
"data": {
"attributes": {
"title": "Wish You Were Here",
"release_date": "1975-09-15",
"release_date_accuracy": 1
"notes": "",
"group_id": 3455
}
}
# in albums_controller.rb
def album_params
params.require(:data).require(:attributes)
.permit(:title, :group_id, :release_date, :release_date_accuracy, :notes)
end
From looking at https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html I think the data format that Rails is normally going to expect will look something like:
{
"group": {
"name": "Pink Floyd",
"notes": "",
"roles_attributes": [
{ "artist_id": 3445 },
{ "artist_id": 3447 }
]
}
}
with a permit statement that looks something like (note the . before permit has moved):
params.require(:group).
permit(:name, :notes, roles_attributes: [:artist_id])
I think you have a few options here:
Change the data format coming into the action.
Craft a permit statement that works with your current data (not sure how tricky that is), you can test your current version in the console with:
params = ActionController::Parameters.new({
"data": {
"attributes": {
"name": "Pink Floyd",
"notes": "",
},
"relationships": {
"artists": [{ type: "artist", "id": 3445 }, { type: "artist", "id": 3447 }]
}
}
})
group_params = params.require(:data).
permit(attributes: [:name, :notes],
relationships: [:artists])
group_params.to_h.inspect
and then restructure the data to a form the model will accept; or
Restructure the data before you try to permit it e.g. something like:
def group_params
params_hash = params.to_unsafe_h
new_params_hash = {
"group": params_hash["data"]["attributes"].merge({
"roles_attributes": params_hash["data"]["relationships"]["artists"].
map { |a| { "artist_id": a["id"] } }
})
}
new_params = ActionController::Parameters.new(new_params_hash)
new_params.require(:group).
permit(:name, :notes, roles_attributes: [:artist_id])
end
But ... I'm sort of hopeful that I'm totally wrong and someone else will come along with a better solution to this stuff.

ActiveModel::Serializers JSON API nested associations include?

I haven't found the exactly same question on Stackoverflow. Besides AMS changes so rapidly, that even 2-year-old answers get outdated often.
I use Rails 5 API-only and the gem 'active_model_serializers' (AMS) (ver. 0.10.6).
I also use the JSONAPI response format - not simply JSON.
I need to render a nested include - not just nested relations as of now.
Code example:
serializer1:
class QuestionSerializer < ActiveModel::Serializer
attributes :id, :title, :created_at, :updated_at
belongs_to :user
end
serializer2:
class UserSerializer < ActiveModel::Serializer
attributes :id, :email
has_many :cities
end
serializer3:
class CitySerializer < ActiveModel::Serializer
attributes :id, :name
end
controller:
def index
#questions = Question.all
render json: #questions, include: [:user, 'user.city']
end
I get this response:
{
"data": [
{
"id": "1",
"type": "questions",
"attributes": {
"title": "First",
"created-at": "2017-03-27T13:22:15.548Z",
"updated-at": "2017-03-27T13:22:16.463Z"
},
"relationships": {
"user": {
"data": {
"id": "3",
"type": "users"
}
}
}
}
],
"included": [
{
"id": "3",
"type": "users",
"attributes": {
"email": "client2#example.com"
},
"relationships": {
"cities": {
"data": [
{
"id": "75",
"type": "cities"
}
]
}
}
}
]
}
I really do get a nested relation city. I even get the city id.
But the problem is - how do I get other city attributes like name? I need another include section - maybe inside current include section (nested?).
How to do that? (without any additional gems)
I found some solution. I don't know about how clean is it. It is based on 2 prerequisites:
https://github.com/rails-api/active_model_serializers/blob/db6083af2fb9932f9e8591e1d964f1787aacdb37/docs/general/adapters.md#included
ActiveModel Serializers: has_many with condition at run-time?
I applied the conditional relations and user-all-permissive include:
controller:
#questions = Question.all
render json: #questions,
show_user: (param? params[:user]),
show_cities: (param? params[:city]),
include: [:user, "user.**"]
serializer1:
class QuestionSerializer < ActiveModel::Serializer
attributes :id, :title, :created_at, :updated_at
belongs_to :user, if: -> { should_render_user }
def should_render_user
#instance_options[:show_user]
end
end
serializer2:
class UserSerializer < ActiveModel::Serializer
attributes :id, :email
has_many :cities, if: -> { should_render_cities }
def should_render_cities
#instance_options[:show_cities]
end
end
serializer3:
class CitySerializer < ActiveModel::Serializer
attributes :id, :name
end
helper:
def param? param
param && param != "false" && param != "nil"
end
Conditional relations allow to control which include's actually to render.
The following should do it
render json: #questions, include: [:categories, user: :city]
You should also include the user in the parameter 'fields' and using strings instead of symbols for the parameter include
render json: #questions, fields: [:title, :user], include: ['user', 'user.cities']

Active Model Serializer not working with json_api adapter

I am trying to use custom serializers for the relationships in a serializer and the json_api adapter enabled. However the relationships are not serialized correctly (or, better, not at all displayed/serialized).
PostController.rb
def index
render json: Post.all, each_serializer: Serializers::PostSerializer
end
Serializer
module Api
module V1
module Serializers
class PostSerializer < ActiveModel::Serializer
attributes :title, :id
belongs_to :author, serializer: UserSerializer
has_many :post_sections, serializer: PostSectionSerializer
end
end
end
end
JSON output:
{
"data": [
{
"attributes": {
"title": "Test Title"
},
"id": "1",
"relationships": {
"author": {
"data": {
"id": "1",
"type": "users"
}
},
"post_sections": {
"data": [
{
"id": "1",
"type": "post_sections"
}
]
}
},
"type": "posts"
}
]
}
As you can see, the relationships are not fulfilled, which happens only if I specify a custom serializer for the relationships!!
If I do something like this:
module Api
module V1
module Serializers
class PostSerializer < ActiveModel::Serializer
attributes :title, :id
belongs_to :author # no custom serializer!
has_many :post_sections # no custom serializer!
end
end
end
end
The relationships are shown correctly, but not using a custom serializer...
What's the issue here ?
EDIT
According to the json API 1.0 Format, what I am getting back is the so-called resource identifier object.
The following primary data is a single resource identifier object that
references the same resource:
{ "data": {
"type": "articles",
"id": "1" } }
Is there a way to prevent getting resource identifier objects, and get the actual data instead ?
Relationships only returns id and type according to json-api exmaples. If you need to return more information about this relation you should add include option on your render action.
Ex.
PostController.rb
class PostsController < ApplicationController
def show
render json: #post, include: 'comments'
end
end
Serializers
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :content
has_many :comment, serializer: CommentSerializer
end
class CommentSerializer < ActiveModel::Serializer
attributes :id, :title, :content
end
JSON output:
{
"data": {
"id": "1",
"type": "post",
"attributes": {
"title": "bla",
"content": "bla"
},
"relationships": {
"comment": {
"data": [
{
"type": "comments",
"id": "1"
}
]
}
}
},
"included": {
{
"id": "1",
"type": "comments",
"attributes": {
"title": "test",
"content": "test"
}
}
]
}
Just to add to #Bruno Bacarini's answer, you may also include chained associations by using:
render #posts, include: ['authors.profile', 'comments']
source: joaomdmoura's comment on github

Active model serializers - custom adapter

I would like to refactor an existing rails API using active model serializers.
Unfortunately the existing API uses a slightly different JSON schema than any of the the existing adapters and I have been unable to recreate it using AMS.
The schema I am trying to recreate is like this:
{
"status": 0,
"message": "OK",
"timestamp": 1438940571,
"data": {
"contacts": [
{
"contact": {
"id": "1",
"first_name": "Kung Foo",
"last_name": "Panda",
"created_at": "2015-07-23T14:09:20.850Z",
"modified_at": "2015-08-04T15:21:36.639Z"
}
},
{
"contact": {
"id": "2",
"first_name": "Johnny",
"last_name": "Bravo",
"created_at": "2015-07-23T14:09:20.850Z",
"modified_at": "2015-08-04T15:21:36.639Z"
}
}
]
}
}
I am wondering is there a way to create a custom adapter for active model serializers, or otherwise create this schema.
Could just use a couple of serializers.
class MessageSerializer < ActiveModel::Serializer
attributes :status, :message, :timestamp, :data
# We could've just used that, were it not for nested hashes
# has_many :contacts, key: :data
attributes :data
def data
ActiveModel::ArraySerializer.new(object.contacts, root: 'contacts')
end
end
class ContactSerializer < ActiveModel::Serializer
attributes :id, :first_name, :last_name, :created_at, :modified_at
def root
'contact'
end
end
There seems to be no better way to do that
Then somewhere in your controller:
render json: Message.serializer.new(#message, root: false)

Reduce complexity of RABL template

My RABL template seems to be very un-DRY and over complex. Because of this I think I may be using it wrong, or that there are better ways at generating my desired output.
As you can see from the show.rabl code, I have to turn the plugins_vulnerability.vulnerability association into a JSON hash, explicitly selecting which keys I need, then merge the plugins_vulnerability.fixed_in value into the hash, and finally adding the new hash, which now contains the fixed_in value, to the vulnerabilities_array array.
I'm doing this because I want the fixed_in value to be within the vulnerability node.
plugins_controller.rb
class Api::V1::PluginsController < Api::V1::BaseController
def show
#plugin = Plugin.friendly.includes(:plugins_vulnerability, :vulnerabilities).find(params[:id])
end
end
show.rabl:
object #plugin
cache #plugin if Rails.env == 'production'
attributes :name
# Add the 'vulnerabilities' node.
node :vulnerabilities do |vulnerabilities|
vulnerabilities_array = []
# turn the plugins_vulnerability association into an array
vulnerabilities.plugins_vulnerability.to_a.each do |plugins_vulnerability|
vulnerability = plugins_vulnerability.vulnerability.as_json # turn the plugins_vulnerability.vulnerability association into json
vulnerability = vulnerability.select {|k,v| %w(id title references osvdb cve secunia exploitdb created_at updated_at metasploit fixed_in).include?(k) } # only select needed keys
vulnerabilities_array << {
:vulnerability => vulnerability.merge(:fixed_in => plugins_vulnerability.fixed_in)
} # merge the fixed_in attribute into the vulnerability hash and add them to an array (fixed_in is from plugins_vulnerabilities)
end
vulnerabilities_array
end
output.json
{
"plugin": {
"name": "simple-share-buttons-adder",
"vulnerabilities": [
{
"vulnerability": {
"id": 88157,
"title": "Simple Share Buttons Adder 4.4 - options-general.php Multiple Admin Actions CSRF",
"references": "https:\/\/security.dxw.com\/advisories\/csrf-and-stored-xss-in-simple-share-buttons-adder\/,http:\/\/packetstormsecurity.com\/files\/127238\/",
"osvdb": "108444",
"cve": "2014-4717",
"secunia": "",
"exploitdb": "33896",
"created_at": "2014-07-15T17:16:51.227Z",
"updated_at": "2014-07-15T17:16:51.227Z",
"metasploit": "",
"fixed_in": "4.5"
}
},
{
"vulnerability": {
"id": 88158,
"title": "Simple Share Buttons Adder 4.4 - options-general.php ssba_share_text Parameter Stored XSS Weakness",
"references": "https:\/\/security.dxw.com\/advisories\/csrf-and-stored-xss-in-simple-share-buttons-adder\/,http:\/\/packetstormsecurity.com\/files\/127238\/",
"osvdb": "108445",
"cve": "",
"secunia": "",
"exploitdb": "33896",
"created_at": "2014-07-15T17:16:51.341Z",
"updated_at": "2014-07-15T17:16:51.341Z",
"metasploit": "",
"fixed_in": "4.5"
}
}
]
}
}
I guess you can do something like this:
object #plugin
cache #plugin if Rails.env == 'production'
attributes :name
child(#plugin.vulnerabilities => :vulnerabilities) {
attributes :id, :title, :references, :osvdb, :cve, :secunia, :exploitdb, :created_at, :updated_at, :metasploit
# Add the 'fixed_in' node.
node :fixed_in do |vulnerability|
#plugin.plugins_vulnerability.fixed_in
end
}
This should create the same output that you need. And it doesn't look awefully complex to me.

Resources