I am a bit lost getting on board fast_jsonapi / active_model_serializers to build an API. I have the basics down but seem stuck on a custom solution.
I have this as a serializer:
class AreaSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :name, :cost_center, :notes
has_many :children
end
In my area model I have:
has_many :children, -> { Area.where(ancestry: id) }
My controller looks like:
class Api::V1::AreasController < ApiController
def index
render json: AreaSerializer.new(Area.root).serialized_json
end
end
Areas are nested in a hierarchy with the ancestry gem. The output is:
{
"data": [{
"id": "1",
"type": "area",
"attributes": {
"id": 1,
"name": "Calgary",
"cost_center": "123456",
"notes": ""
},
"relationships": {
"children": {
"data": [{
"id": "3",
"type": "child"
}]
}
}
}, {
"id": "2",
"type": "area",
"attributes": {
"id": 2,
"name": "Edmonton",
"cost_center": "78946",
"notes": ""
},
"relationships": {
"children": {
"data": []
}
}
}]
}
I am looking for an out put like this:
{
"data": [{
"id": "1",
"type": "area",
"attributes": {
"id": 1,
"name": "Calgary",
"cost_center": "123456",
"notes": ""
},
"relationships": {
"areas": {
"data": [{
"id": "3",
"type": "area",
"attributes": {
"id": 3,
"name": "Child Area",
"cost_center": "123456",
"notes": ""
}
}]
}
}
}, {
"id": "2",
"type": "area",
"attributes": {
"id": 2,
"name": "Edmonton",
"cost_center": "78946",
"notes": ""
}
}]
}
The idea being where the nested relationship shows the details etc.
I just started using fast_jsonapi in my rails project.
fast_jsonapi adheres to JSON:API spec which you can see here.
So, you will not be able to use the relationship helper functions (:has_many, :belongs_to) to achieve the output you want, which is nesting the attributes of area inside the relationships key.
"relationships": {
"areas": {
"data": [{
"id": "3",
"type": "area",
"attributes": { // nesting attributes of area
"id": 3,
"name": "Child Area",
"cost_center": "123456",
"notes": ""
}
}]
}
}
JSON:API specifies that:
To reduce the number of HTTP requests, servers MAY allow responses
that include related resources along with the requested primary
resources. Such responses are called “compound documents”.
So instead, you will have the attributes of area inside a key called included.
In order to get the included key in your response, you will need to provide an options hash in your controller to the serializer.
I don't quite understand your model Area relationships, but assume an Area has many SubArea.
class Api::V1::AreasController < ApiController
def index
// serializer options
options = {
include: [:sub_area],
collection: true
}
render json: AreaSerializer.new(Area.root, options).serialized_json
end
end
You Cannot use Association in fast_jsonapi . To Get a response in nested format . You need to add methods and need to create another serializer .
class AreaSerializer
include FastJsonapi::ObjectSerializer
set_type 'Area'
attributes :id, :name, :cost_center, :notes
attribute :childrens do |area, params|
ChildrenSerializer.new(area.childrens, {params:
params})
end
end
class ChildrenSerilizer
include FastJsonapi::ObjectSerializer
set_type 'Area'
attributes :id, :name ...
end
I started using the technique listed above but ended up forking and re-writing the jsonapi-serializer gem so it allows nesting (up to 4 levels deep) and does away with the concept of having relationships and attributes keys. I was also frustrated that it only output ID and TYPE keys of which TYPE is redundant most of the time since the object typically stays the same class in an array as an example and people seldom use real polymorphism (although it still supports this and outputs ID/type for polymorphic relationships).
Probably the best part of my re-write is that it allows deterministic field select-ability anywhere within the json key tree via a new fields input.
https://github.com/rubesMN/jsonapi-serializer
Related
I am creating a travel app which uses a backend Rails API. I've decided to use the Fast JSON API to serialize my data. I have a list of countries; each country has many cities, and each city has many attractions.
These are a list of my associations between my models.
Country.rb
has_many :cities
has_many :attractions, through: :locality
Localities.rb
has_many :attractions
belongs_to :country
Attraction.rb
belongs_to :locality
When I serialize my data for an individual attraction, I would like to include only the name attribute of the city and the name attribute of the country it belongs to. I am currently doing this by adding the optional parameter to include the locality and country name.
def show
attraction = Attraction.find_by(slug: params[:slug])
options = {}
options[:include] = [:locality, :'locality.country.name']
render json: AttractionSerializer.new(attraction, options).serialized_json
end
However, this gives all the attributes and relationships of the country, including a list of all unrelated localities nested within the country, which will become really inefficient when my dataset becomes larger. See below:
{
"data": {
"id": "1",
"type": "attraction",
"attributes": {
"name": "Plaza de España",
"description": "Site of the World Exposition in 1929",
"types": null,
"latitude": 40.4232824,
"longitude": -3.7107257,
"slug": "plaza-de-espana",
"locality": {
"id": 1,
"name": "Seville",
"country_id": 168,
"created_at": "2020-06-10T05:43:47.474Z",
"updated_at": "2020-06-10T05:43:47.474Z",
"slug": "seville",
"latitude": 37.3886303,
"longitude": -5.9953403
}
},
"relationships": {
"locality": {
"data": {
"id": "1",
"type": "locality"
}
}
}
},
"included": [
{
"id": "1",
"type": "locality",
"attributes": {
"name": "Seville",
"latitude": 37.3886303,
"longitude": -5.9953403,
"slug": "seville"
},
"relationships": {
"country": {
"data": {
"id": "168",
"type": "country"
}
}
}
},
{
"id": "168",
"type": "country",
"attributes": {
"name": "Spain",
"slug": "spain",
"iso_3166_1_alpha2": "ES",
"iso_3166_1_alpha3": "ESP",
"iso_3166_1_numeric": "724"
},
"relationships": {
"localities": {
"data": [
{
"id": "1",
"type": "locality"
},
{
"id": "2",
"type": "locality"
},
{
"id": "3",
"type": "locality"
},
{
"id": "4",
"type": "locality"
},
{
"id": "5",
"type": "locality"
},
{
"id": "6",
"type": "locality"
}
]
},
"attractions": {
"data": [
{
"id": "1",
"type": "attraction"
}
]
}
}
}
]
}
Is there a way to only include just one attribute (i.e. name of Country) in the Attraction JSON, instead of the whole object? (Attraction is nested two levels below Country)
Thank you very much.
You need to create multiple serializers that work together to achieve this.
class AttractionSerializer
include FastJsonapi::ObjectSerializer
attributes :attraction_attr_1, :attraction_attr_2, etc.
belongs_to :locality, serializer: AttractionLocalitySerializer
end
class AttractionLocalitySerializer
include FastJsonapi::ObjectSerializer
attributes :name
belongs_to :country, serializer: AttractionCountrySerializer
end
class AttractionCountrySerializer
include FastJsonapi::ObjectSerializer
attributes :name
end
Continuing my previous question at: Active Model Serializer and Pundit deleting records during a Show CRUD action
I have a situation where a User should not be able to view another user's unpublished chapters belonging to a Story an author created.
E.g. If UserA creates a story called Targon and provides 2 published chapters and 2 unpublished chapters, then UserB should only see published chapters for Targon story.
Normally with Pundit policy scoping, it scopes the index CRUD action.
What I need to scope however, are Chapters belonging to a Story during the render json line:
render json: story, include: [:user, :chapters], status: :ok
I have tried:
# ---------------------------------------------------------------------------
# ActiveRecord auto-save will kick in and delete all unpublished chapters
# ---------------------------------------------------------------------------
story.chapters = policy_scope(story.chapters)
render json: story, include: [:user, :chapters], status: :ok
According to https://gist.github.com/demisx/9896113 (has_many section) the above code will delete all the unpublished chapters belonging to Targon when I reassign story.chapters:
story.chapters = policy_scope(story.chapters) # BAD
I am hoping there is some way I can do something like this:
render json: story, include: [:user, policy_scope(:chapters)], status: :ok
At the moment, without scoping the story.chapters any users who fetch for Story with ID 16 (Targon) will get back JSONAPI:
{
"data": {
"id": "16",
"type": "stories",
"attributes": {
"title": "Mount Targon",
"summary": "Mount Targon is the mightiest peak in Runeterra, a towering peak of sun-baked rock amid a range of summits unmatched in scale anywhere else in the world. Located far from civilization, Mount Targon is utterly remote and all but impossible to reach save by the most determined seeker. Many legends cling to Mount Targon, and, like any place of myth, it is a beacon to dreamers, madmen and questors of adventure. Some of these brave souls attempt to scale the impossible mountain, perhaps seeking wisdom or enlightenment, perhaps chasing glory or some soul-deep yearning to witness its summit. The ascent is all but impossible, and those hardy few who somehow survive to reach the top almost never speak of what they have seen. Some return with a haunted, empty look in their eyes, others changed beyond all recognition, imbued by an Aspect of unearthly, inhuman power with a destiny few mortals can comprehend.",
"published": true,
"published-date": "2017-11-02T10:35:33.184Z",
"created-at": "2017-11-02T10:35:33.184Z",
"updated-at": "2017-11-04T07:35:04.083Z",
"cover": {
"url": "http://res.cloudinary.com/chewedon/image/upload/v1509780931/c8ubn3tfivxziyxwynsa.png",
"standard": {
"url": "http://res.cloudinary.com/chewedon/image/upload/c_fill,g_north,h_300,w_200/c8ubn3tfivxziyxwynsa.png"
}
}
},
"relationships": {
"user": {
"data": {
"id": "1",
"type": "users"
}
},
"chapters": {
"data": [{
"id": "26",
"type": "chapters"
}, {
"id": "27",
"type": "chapters"
}, {
"id": "37",
"type": "chapters"
}, {
"id": "38",
"type": "chapters"
}]
}
}
},
"included": [{
"id": "1",
"type": "users",
"attributes": {
"username": "Chewedon",
"photo": {
"url": "http://res.cloudinary.com/chewedon/image/upload/v1509857442/nx1tqlcdxrhz6r3kjx87.jpg",
"standard": {
"url": "http://res.cloudinary.com/chewedon/image/upload/c_fill,g_north,h_150,w_150/nx1tqlcdxrhz6r3kjx87.jpg"
}
}
},
"relationships": {
"stories": {
"data": [{
"id": "1",
"type": "stories"
}, {
"id": "2",
"type": "stories"
}, {
"id": "3",
"type": "stories"
}, {
"id": "4",
"type": "stories"
}, {
"id": "5",
"type": "stories"
}, {
"id": "6",
"type": "stories"
}, {
"id": "8",
"type": "stories"
}, {
"id": "9",
"type": "stories"
}, {
"id": "10",
"type": "stories"
}, {
"id": "11",
"type": "stories"
}, {
"id": "12",
"type": "stories"
}, {
"id": "13",
"type": "stories"
}, {
"id": "14",
"type": "stories"
}, {
"id": "15",
"type": "stories"
}, {
"id": "16",
"type": "stories"
}]
}
}
}]
}
Here in the relationship section, chapters 37 and 38 are unpublished, leading to a 403 Forbidden on my Ember frontend.
Ideally the server should have scoped out these before returning the records but due to the bug I described above and in my previous Stackoverflow question, I am stuck on how to scope included fields with Pundit.
Any ideas?
Thanks to user oowowaee from previous linked question, who suggested overriding the Story serializer's chapters field (which I didn't know you could do that), the code is working now and the records don't get deleted from database.
class StorySerializer < ActiveModel::Serializer
include Pundit
attributes :id, :title, :summary, :published, :published_date, :created_at, :updated_at, :cover
belongs_to :user
has_many :chapters
# ------------------------------------------------------------------------
# Note: need to use 'object.chapters' not 'self.chapters` below.
# ------------------------------------------------------------------------
def chapters
policy_scope(object.chapters)
end
end
I'm trying to write an API endpoint for creating Redemptions in my app.
In Rails, my model is such that Redemption has many Items (class_name: RedemptionItems).
Here's my POST Body, following what I assume is the correct JSONAPI Specification (since there is no explicit spec for creating parent and child records in one request).
{
"data": {
"type": "redemptions",
"relationships": {
"items": {
"data": [{
"type": "items",
"attributes": { "offer_id": "1", "quantity": "2" }
},
{
"type": "items",
"attributes": { "offer_id": "1", "quantity": "3" }
},
{
"type": "items",
"attributes": { "offer_id": "123", "quantity": "3" }
}
]
}
}
}
}
I'm using JSONAPI::Resources. I have defined my JSONAPI::Resources thus:
class Platform::Api::Members::RedemptionItemResource < JSONAPI::Resource
model_name 'Platform::RedemptionItem'
has_one :redemption
end
class Platform::Api::Members::RedemptionResource < JSONAPI::Resource
model_name 'Platform::Redemption'
has_many :items, class_name: 'RedemptionItem'
end
It's currently giving me an inavlid links object error, which doesnt tell me anything on how I must improve my request body.
{
"errors": [
{
"title": "Invalid Links Object",
"detail": "Data is not a valid Links Object.",
"code": "115",
"status": "400"
}
]
}
I'm using Ionic framework, but question is about AngularJS. I have written json api on ruby on rails. For authentication I choose ng-token-auth + devise-token-auth.
User json:
{
"data": {
"id": "1",
"type": "users",
"attributes": {
"name": "User1",
"email": "email#email.com",
"current-sign-in-at": "..",
"last-sign-in-at": "..",
"created-at": ".."
},
"relationships": {
"friends": {
"data": [
{
"id": "2",
"type": "users"
}
]
}
}
},
"included": [
{
"id": "2",
"type": "users",
"attributes": {
"name": "User2",
"email": "user#user.com",
"current-sign-in-at": "..",
"last-sign-in-at": "..",
"created-at": ".."
},
"relationships": {
"friends": {
"data": [ ]
}
}
}
]
}
My serializer for user:
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :email,:current_sign_in_at,
:last_sign_in_at, :created_at
has_many :friends, through: :friendships
end
Current user object as response:
{"success":true,"data":{"id":1,"provider":"email","uid":"0a56bb6b-dc72-4ef3-906e-1c17cb2fef46","name"
:"User1","nickname":null,"image":null,"email":"email#email.com"}}
There is my problem, I can't reach user relationship information.
I don't know why, but object name in my view is user (not current_user) and don't have access for it in my controllers.
Question is how can I have some additional information from this object and how to provide current user object access for my controllers.
I figured it out, so the answer is really simple. ng-token-auth provide $rootScope.user as the current user and it can be used in controllers.
But I still don't know how to provide related data to user object, fortunately found workaround for my needs.
I'm trying to build a JSON API style API using AM::Serializer. I'm running into an issue with sideloading.
I want to be able to build JSON that looks like:
{
"primaries": [{
"id": 123,
"data": "Hello world.",
"links": {
"secondaries": [ 1, 2, 3 ]
}
}],
"linked" : {
"secondaries": [
{
"id": 1,
"data": "test1"
},
{
"id": 2,
"data": "test2"
},
{
"id": 3,
"data": "test3"
}
]
}
}
The code I've been able to come up with looks like:
class PrimarySerializer < ActiveModel::Serializer
attributes :id, :data
has_many :secondaries, key: :secondaries, root: :secondaries
embed :ids, include: true
end
Which generates JSON that looks like:
{
"primaries": [{
"id": 123,
"data": "Hello world.",
"secondaries": [ 1, 2, 3 ]
}],
"secondaries": [
{
"id": 1,
"data": "test1"
},
{
"id": 2,
"data": "test2"
},
{
"id": 3,
"data": "test3"
}
]
}
Is there a way to override the location of the in-element secondaries and sideloaded secondaries such that they live in child nodes link and linked?
The above code is an abstraction of the actual code and may not work. Hopefully it illustrates the point sufficiently.
Thanks!
ActiveModel Serializers can do this. The problem is that the built-in association methods are to restrictive. Instead you must build up the links & linked parts manually.
(This answer refers to the stable 0.8.1 version of ActiveModel Serializers)
Here's a Gist with a complete JSON-API solution https://gist.github.com/mars/97a637560109b8ddfb27
Example:
class ExampleSerializer < JsonApiSerializer # see Gist for superclass
attributes :id, :name, :links
def links
{
things: object.things.map(&:id),
whatzits: object.whatzits.map(&:id)
}
end
def as_json(*args)
hash = super(*args)
hash[:linked] = {
things: ActiveModel::ArraySerializer.new(
object.things,
each_serializer: ThingsSerializer
).as_json,
whatzits: ActiveModel::ArraySerializer.new(
object.whatzits,
each_serializer: WhatzitsSerializer
).as_json
}
hash
end
end