In my application, I‘m using ActiveResource to manage the data that I receive from a remote API. Say, one of my models is called Account and it has a field called settings, which is documented in the API as a “freeform hash”, meaning it can store whatever.
A sample simplified JSON I would receive from the API:
{
"account": {
"title": "My Account",
"settings": {
"a_random_setting": true,
"another_random_setting": 42,
"a_subconfig_of_sorts": {
"is_deep": true
}
}
}
}
ActiveResource very kindly goes all the way down the deepest nested objects in that JSON and turns them all into Ruby objects:
my_account.settings # => Account::Settings
my_account.settings.a_subconfig_of_sorts # => Account::Settings::ASubconfigOfSorts
This makes it a bit difficult to look up dynamic keys within that the content of settings. Essentially, I would much rather have settings as regular hash, and not a custom nested object generated for me on the fly.
my_account.settings.class # => Hash
my_account.settings[:a_subconfig_of_sorts] # => {:is_deep => true}
How do I make ActiveResource do that? My first guess was by using the schema declaration, but that only provides scalar types, it seems.
Made solution that works with that issue. Hope that helps.
class Account < ActiveResource::Base
create_reflection :settings_macro, :settings, class_name: 'Account::Hash'
class Hash < ::Hash
def initialize(hash, persisted)
merge!(hash)
end
end
end
Related
I'm trying to override the 'as_json' method to include nested attributes for an object, but I'm having trouble properly nesting the JSON.
Currently, I have this in Rails for my 'as_json' method.
// User.rb
def as_json(options = {})
json = {:id => id, :name => name,
:settings_attributes => settings.select(:id,:name),
:setting_options_attributes => setting_options.select(:id, :amount)}
json
end
However, setting_options_attributes should be nested under settings_attributes and I can't find the proper syntax do achieve this.
Assuming settings and settings_options are already hashes, you should be able to do this:
// User.rb
def as_json(options = {})
{
id: id,
name: name,
settings_attributes: settings.select(:id,:name).merge({
setting_options_attributes: setting_options.select(:id, :amount)
},
}
end
If settings and settings_options are Models, then you probably need to do something like this:
// User.rb
def as_json(options = {})
{
id: id,
name: name,
settings_attributes: settings.select(:id,:name).map(&:attributes),
}
end
// Setting.rb
def attributes
{
id: id,
name: name,
setting_options_attributes: setting_options.select(:id,:amount).map(&:attributes),
}
end
It's a little confusing in that SettingOption appears to belong_to User (as you reference it directly) yet you want to nest it within Setting, which implies that it is a "belongs_to :through" relationship, at which point I think you should make Setting responsible for the nesting.
Lastly, if you want to develop complex JSON output, rather than override as_json, you should consider using jbuilder (which is typically bundled with rails) as it moves your JSON logic out of your model and into the view, which is arguably the more appropriate place to be constructing how you view your data. JBuilder is much better at designing complex JSON.
I'm returning a resource in API (I'm, using Grape) but I would like to return also the relationship objects (as included objects just as an ember application expects). How can I achieve that ? My serializer is as follow:
class JudgeSerializer < ActiveModel::Serializer
attributes :ID, :FIRST_NAME, :LAST_NAME, :FULL_NAME
end
My model:
class Judge < ApplicationRecord
self.primary_key = 'id'
self.table_name = 'judge'
has_many :judgecourts, class_name: 'Judgecourt', primary_key: 'ID',foreign_key: 'JUDGE_ID'
end
I'm returning this resource this way:
desc 'Return a specific judge'
route_param :id do
get do
judge = Judge.find(params[:id])
present judge
end
end
It would be good to generate something like this:
data
:
{type: "judges", id: "1", attributes: {…}, relationships: {…}}
included
:
Array(1)
0
:
{type: "judgecourts", id: "1", attributes: {…}}
Well it looks like there are two topics in your question:
1. How to include a relationship in a ActiveModelSerializer
This can be done pretty easily by adjusting your active model serializer and adding the relation, so that ActiveModelSerializer knows that it must include the relation in the serialized objects:
class JudgeSerializer < ActiveModel::Serializer
attributes :ID, :FIRST_NAME, :LAST_NAME, :FULL_NAME
has_many :judgecourts
end
This would automatically provide the judgecourts relation inside the serialized json. An example with the classic Post/Comment objects:
2. Use specific format...
The format you specified looks a lot like JSON:API format. If that's what you're really wanting to achieve, then you might be better off using the JSON:API adapter built-in ActiveModelSerializer. For this, you need to tell AMS to use the proper adapter, maybe through an initializer file :
# ./initializers/active_model_serializer.rb
ActiveModelSerializers.config.adapter = :json_api
After this, your json should be formatted the way you seem to expect. I'm no expert in the json api specification, so there might be some more things to tweak. You'll be able to find more information about this adapter in ActiveModelSerializers wiki page on adapters, section JSON API.
Here's what I get with my Post/Comment example:
Note that there are other gems out there that were built to specifically address the JSON API specification, such as jsonapi-rb and Netflix Fast JSON API. They might be of interest for you.
In a Ruby 2/Rails 4 app, I am trying to use acts-as-taggable-on in conjunction with active_model_serializers in order to create a JSON API that would output my tags along with other model parameters.
First some background/motivation for this issue: The JSON is being fed into ember/ember-data, which as of the time of this writing has removed support for embedded records in JSON. There is a supposed fix in the documentation about this, but I find it clumsy and also haven't actually gotten it to work. Since I'm an Ember newbie and am a little more comfortable with Rails, I figure I'll try to solve the problem another way, by sideloading the tag record alongside the document record. I like this solution better because it makes more sense for my application, but I can't get this to work either.
Here's an example: Let's say I have a Document model that is using acts-as-taggable-on:
class Document < ActiveRecord::Base
acts_as_taggable
# other code omitted
I've set up the database with one document with one tag. Now we consider the following cases:
1. Full Object Embed: With the following serializer:
class DocumentSerializer < ActiveModel::Serializer
attributes :id
has_many :tags
My JSON has the following format (using Rails 4 UUIDs):
{
"documents": [
{
"id": "c41460fa-2427-11e3-8702-0800270f33f4",
"tags": [
{
"id": "a33fc396-2428-11e3-8eeb-0800270f33f4",
"name": "test"
}
]
}
]
}
2. ID Embed: Using the serializer
class DocumentSerializer < ActiveModel::Serializer
attributes :id
has_many :tags, embed: :id
My JSON now looks like:
{
"documents": [
{
"id": "c41460fa-2427-11e3-8702-0800270f33f4",
"tag_ids": [
"a33fc396-2428-11e3-8eeb-0800270f33f4"
]
}
]
}
3. ID Embed with Tags Sideloaded: According to active_model_serializers documentation, I should be able to do
class DocumentSerializer < ActiveModel::Serializer
attributes :id
has_many :tags, embed: :id, include: true
but this doesn't work. Instead, I get a NoMethodError:
undefined method `object' for #<ActsAsTaggableOn::Tag:0x007f258cf1db38>
I've tried searching for this problem but haven't found anything useful so far. I also couldn't find any documentation on either gem regarding usage with the other gem. My suspicion right now that it has something to do with how acts-as-taggable-on is implemented, that it's not a straightforward has_many relationship? Is anybody able to offer some input on this issue? Thanks in advance!
Fixed! Turns out to sideload the tags, the corresponding serializer must be defined. I did not know this, because the documentation seems to imply that having a serializer is optional, and some default will be used in the absence of one. Apparently this is not the case if you wish to use the include: true option. Key in sight came from here, thanks very much!
For completeness, I'll show what I did. I created tag_serializer.rb with the following code:
module ActsAsTaggableOn
class TagSerializer < ActiveModel::Serializer
attributes :id, :name
end
end
and now my JSON:
{
"tags": [
{
"id": "a33fc396-2428-11e3-8eeb-0800270f33f4",
"name": "test"
}
],
"documents": [
{
"id": "c41460fa-2427-11e3-8702-0800270f33f4",
"tag_ids": [
"a33fc396-2428-11e3-8eeb-0800270f33f4"
]
}
]
}
I have a simple ActiveRecord class that logs an event and stores some additional data using the serialize method. It has one string column for an event and a text column to store a data object.
# DB Columns
# event => string
# data => text
#
class MyLog < ActiveRecord::Base
serialize :data
validates :event, :data, :presence => true
end
In my controller I would like to take user submitted information and include it as the data:
class ContactFormController < ApplicationController
def send_message
...
data = {name: params[:name], email: params[:email], message: params[:message]}
MyLog.create(event: "User submitted contact form", data: data)
...
end
end
Questions
The serialize method uses YAML by default to store objects like this. Is there a security risk here in the case that a user submits some crafty code that is passed through the parameters? Is there a chance for user submitted Ruby code to be executed when the data field is retrieved and deserialized?
My goal is to provide a way to log events from anywhere in my app and store data of any sort about that event. Is there a better way to preform this than what I have set up here?
In general terms, it's perfectly fine to serialize anything you want. User data of any kind is acceptable.
This presumes you're patched up to the absolutely latest version of Rails 3.2 or 4.0. There have been some issues with YAML and JSON serialization in the past but these have been patched and resolved.
Test your application against known vulnerabilities using a tool like GemCanary to be sure you're current, and to catch future problems.
I'm running into an issue where I'm working with the as_json method, and how to efficiently return the object in JSON AND it's belongs_to object as JSON as well, where the belongs_to object has its own belongs_to object. Code would probably explain it better.
The Not-Working Way
Alert class
class Alert < ActiveRecord::Base
belongs_to :message
# for json rendering
def as_json(options={})
super(:include => :message)
end
end
Message class
def as_json(options={})
super( methods: [:timestamp, :num_photos, :first_photo_url, :tag_names],
include: { camera: { only: [:id, :name] },
position: { only: [:id, :name, :address, :default_threat_level ]},
images: { only: [:id, :photo_url, :is_hidden]} })
end
The problem with this first set up is that when I have an Alert object and call
alert.as_json()
I get all the attributes from Alert and all the attributes from Message, but none of the other attributes from Message that I want, like Camera, Position, etc.
Here's the "It's Working, But Probably Not Proper Design Way"
Alert Class
class Alert < ActiveRecord::Base
belongs_to :message
# for json rendering
def as_json(options={})
super().merge(:message => message.as_json)
end
end
Messages Class
# for json rendering
def as_json(options={})
super( methods: [:timestamp, :num_photos, :first_photo_url, :tag_names])
.merge(:camera => camera.as_json)
.merge(:position => position.as_json)
.merge(:images => images.as_json)
end
In this 2nd setup, I get all of Messages's nested attributes like I want.
My question, am I missing some Rails Convention to do this properly? It seems like there would/should be an easier way.
The best answer for me was using serializable_hash. #kikito touched on this in his comment, but there was a typo that prevented it from working. It's not serialized_hash, it's serializable_hash.
Literally just find + replace as_json with serializable_hash and this bug goes away. (It's still not fixed in today's Rails 4.0.2). You also get the benefit of having an easier time implementing an XML API later (some people still use those!).
Which version of Rails are you using? This is a known bug in older versions of Rails, supposedly fixed with this pull request. Your syntax looks right to me, so perhaps this is your problem?
As an aside, you may also want to checkout the new active_model_serializers from Jose Valim (Rails core member). It may at least enable you to work around your issue in a more elegant manner.
I would recommend you to take a look at RABL (stands for Ruby API Builder Language) gem (railscast, github). It offers you a DSL for defining the structure of your JSON (and also XML) response in templates (like Haml or CoffeeScript does). It also supports partials.