Nested JSON to Ruby class (with validation) - json

I have the following JSON:
{
"ordernumber":"216300001000",
"datecreated":"2016-11-08T14:23:06.631Z",
"shippingmethod":"Delivery",
...
"customer":{
"firstname":"Victoria",
"lastname":"Validator"
},
"products":[
{
"sku":"ABC1",
"price":"9.99"
},
...
]
}
With the corresponding Ruby classes including validators:
class Task
include ActiveModel::Model
include ActiveModel::Serializers::JSON
validates ..., presence: true
...
end
class Product
include ActiveModel::Model
include ActiveModel::Serializers::JSON
validates ..., presence: true
...
end
class Customer
include ActiveModel::Model
include ActiveModel::Serializers::JSON
validates ..., presence: true
...
end
What I want to do is serialise the JSON to a Ruby class. The problem is that the Task class get's initialised correctly. But the nested classes like Customer and Product remain hashes. (A Task has one Customer and multiple Products)
Example:
json = %Q{{ "ordernumber":"216300001000", "datecreated":"2016-11-08T14:23:06.631Z", "shippingmethod":"Delivery", "customer":{ "firstname":"Victoria", "lastname":"Validator" }, "products":[ { "sku":"ABC1", "price":"9.99" } ] }}
task = Task.new()
task.from_json(json)
task.class
# => Task
task.products[0].class
# => Hash
How do I do this using ActiveModel and also validate the nested JSON? (I'm not using Rails)

As far as I know, ActiveModel::Model brings validations and other handy stuff, but it does not bring tools to handle association problems like this one. You have to implement his behavior yourself.
First of all, I'd use the builtin initialization system that ActiveModel::Model provides. Then I'd define products= and customer= to take the attributes and initialize instances of the proper classes. And call the validations of the associated records.
class Task
include ActiveModel::Model
attr_reader :products, :customer
# ...
validate :associated_records_are_valid
def products=(ary)
#products = ary.map(&Product.method(:new))
end
def customer=(attrs)
#customer = Customer.new(attrs)
end
private
def associated_records_are_valid
products.all?(&:valid?) && customer.valid?
end
end
attributes = JSON.parse(json_str)
task = Task.new(attributes)

Look at this topic: Is it possible to convert a JSON string to an object?. I am not in front of a computer right now to post a code, but I think that answer solves your problem.

Related

Rails 5 Column Named Root Misbehaving

I am attempting to create a restful API in Rails 5, one of my classes has an attribute named root. This root attribute has caused a few errors, I will provide one example. Is root a protected attribute name in rails or ruby?
class ObjectsController < ApplicationController
before_action only: %i[create]
def create
#object = Object.create(object_params)
render json: #object
end
private
def object_params
params.require(:object).permit(:id, :root)
end
When I post to /objects/
with something like the following:
{"object": {"id": "manual_id" , "root": "manual_root"}}
I am returned with the following in Postman:
{
"id": "manual_id",
"root": null,
"extension": "manual_extension"
}
But in the MySQL database the value for root is manual_root.
Has anyone any ideas of what the issue may be.
The solution was to add an attribute alias to the model like so:
class Object < ApplicationRecord
alias_attribute :object_root, :root
end
and use this attribute in the object serializer class:
class ObjectSerializer < ActiveModel::Serializer
attributes :id, :object_root
end
So my problem was with the active model serializer, I believe that root is a reserved keyword. Here is a similar issue: https://github.com/rails-api/active_model_serializers/issues/1135

How do you Skip an Object in an ActiveModel Serializer Array?

I have searched through all the active model serializer (v 0.9.0) documentation and SO questions I can find, but can't figure this out.
I have objects which can be marked as "published" or "draft". When they aren't published, only the user who created the object should be able to see it. I can obviously set permissions for "show" in my controller, but I also want to remove these objects from the json my "index" action returns unless it is the correct user. Is there a way to remove this object from the json returned by the serializer completely?
In my activemodel serializer, I am able to user filter(keys) and overloaded attributes to remove the data, as shown using my code below, but I can't just delete the entire object (I'm left having to return an empty {} in my json, trying to return nil breaks the serializer).
I'm probably missing something simple. Any help would be much appreciated!
class CompleteExampleSerializer < ExampleSerializer
attributes :id, :title
has_many :children
def attributes
data = super
(object.published? || object.user == scope || scope.admin?) ? data : {}
end
def filter(keys)
keys = super
(object.published? || object.user == scope || scope.admin?) ? keys : {}
end
end
That looks correct, try returning an array instead of a hash when you dont want any keys. Also, I don't think calling super is necessary b/c the filter takes in the keys.
Also, I don't think defining an attributes method is necessary.
I have chapters that can either be published or unpublished. They're owned by a story so I ended doing something like below.
has_many :unpublished_chapters, -> { where published: false }, :class_name => "Chapter", dependent: :destroy
has_many :published_chapters, -> { where published: true }, :class_name => "Chapter", dependent: :destroy
Inside of my serializer, I choose to include unpublished_chapters only if the current_user is the owner of those chapters. In ams 0.8.0 the syntax is like so.
def include_associations!
include! :published_chapters if ::Authorization::Story.include_published_chapters?(current_user,object,#options)
include! :unpublished_chapters if ::Authorization::Story.include_unpublished_chapters?(current_user,object,#options)
end
In my case, it's not so bad to differentiate the two and it saves me the trouble of dealing with it on the client. Our situations are similar but say you want to get all of the chapters by visiting the chapters index route. This doesn't make much sense in my app but you could go to that controller and render a query on that table.

rails: active model serializer looking for wrong serializer

When I call the the create method i get a nameError.
Failure/Error: post :create, { user: { email: 'charles#example.com',
NameError:
uninitialized constant API::V1::UsersController::UserV1Serializer
why is it adding that UsersController module to the class it is looking for? In my sessions controller I use the same exact render line and it doesn't complain. what is going on?
app/controllers/api/v1/users_controller.rb
class API::V1::UsersController < API::V1::BaseController
...
...
def create
user = User.new(user_params)
if user.save
sign_in :user, user, store: false
end
render json: user, serializer: UserV1Serializer, root: 'user'
end
app/serializers/api/v1/user_v1_serializer.rb
class API::V1::UserV1Serializer < ActiveModel::Serializer
attributes :id, :email
def attributes
hash = super
if scope == object
hash[:token] = object.authentication_token
end
hash
end
end
Instead of specifying UserV1Serializer, you need to specify the full namespaced class name API::V1::UserV1Serializer.
If you just specify the UserV1Serializer, its looking for the serializer class within current controller API::V1::UsersController::UserV1Serializer which is why you get an error as
uninitialized constant API::V1::UsersController::UserV1Serializer.
Use this instead:
render json: user, serializer: API::V1::UserV1Serializer, root: 'user'

Active Record 4.x and MySQL table column type without using migrations

I'm doing some test with Sinatra v1.4.4 and Active Record v4.0.2. I've created a DBase and a table named Company with Mysql Workbench. In table Company there are two fields lat & long of DECIMAL(10,8) and DECIMAL(11,8) type respectively. Without using migrations I defined the Company model as follow:
class Company < ActiveRecord::Base
end
Everything works except the fact that lat and lng are served as string and not as float/decimal. Is there any way to define the type in the above Class Company definition. Here you can find the Sinatra route serving the JSON response:
get '/companies/:companyId' do |companyId|
begin
gotCompany = Company.find(companyId)
[200, {'Content-Type' => 'application/json'}, [{code:200, company: gotCompany.attributes, message: t.company.found}.to_json]]
rescue
[404, {'Content-Type' => 'application/json'}, [{code:404, message:t.company.not_found}.to_json]]
end
end
Active Record correctly recognize them as decimal. For example, executing this code:
Company.columns.each {|c| puts c.type}
Maybe its the Active Record object attributes method typecast?
Thanks,
Luca
You can wrap the getter methods for those attributes and cast them:
class Company < ActiveRecord::Base
def lat
read_attribute(:lat).to_f
end
def lng
read_attribute(:lng).to_f
end
end
That will convert them to floats, e.g:
"1.61803399".to_f
=> 1.61803399
Edit:
Want a more declarative way? Just extend ActiveRecord::Base:
# config/initializers/ar_type_casting.rb
class ActiveRecord::Base
def self.cast_attribute(attribute, type_cast)
define_method attribute do
val = read_attribute(attribute)
val.respond_to?(type_cast) ? val.send(type_cast) : val
end
end
end
Then use it like this:
class Company < ActiveRecord::Base
cast_attribute :lat, :to_f
cast_attribute :lng, :to_f
end
Now when you call those methods on an instance they will be type casted to_f.
Following diego.greyrobot reply I modified my Company class with an additional method. It overrides the attributes method and afterwards typecast the needed fields. Yet something more declarative would be desirable imho.
class Company < ActiveRecord::Base
def attributes
retHash = super
retHash['lat'] = self.lat.to_f
retHash['lng'] = self.lng.to_f
retHash
end
end

Nested strong parameters in rails - AssociationTypeMismatch MYMODEL expected, got ActionController::Parameters()

I'm rendering a model and it's children Books in JSON like so:
{"id":2,"complete":false,"private":false, "books" [{ "id":2,"name":"Some Book"},.....
I then come to update this model by passing the same JSON back to my controller and I get the following error:
ActiveRecord::AssociationTypeMismatch (Book (#2245089560) expected, got ActionController::Parameters(#2153445460))
In my controller I'm using the following to update:
#project.update_attributes!(project_params)
private
def project_params
params.permit(:id, { books: [:id] } )
end
No matter which attributes I whitelist in permit I can't seem to save the child model.
Am I missing something obvious?
Update - another example:
Controller:
def create
#model = Model.new(model_params)
end
def model_params
params.fetch(:model, {}).permit(:child_model => [:name, :other])
end
Request:
post 'api.address/model', :model => { :child_model => { :name => "some name" } }
Model:
accepts_nested_attributes_for :child_model
Error:
expected ChildModel, got ActionController::Parameters
Tried this method to no avail: http://www.rubyexperiments.com/using-strong-parameters-with-nested-forms/
Are you using accepts_nested_attributes_for :books on your project model? If so, instead of "books", the key should be "books_attributes".
def project_params
params.permit(:id, :complete, :false, :private, books_attributes: [:id, :name])
end
I'm using Angular.js & Rails & Rails serializer, and this worked for me:
Model:
has_many :features
accepts_nested_attributes_for :features
ModelSerializer:
has_many :features, root: :features_attributes
Controller:
params.permit features_attributes: [:id, :enabled]
AngularJS:
ng-repeat="feature in model.features_attributes track by feature.id
My solution to this using ember.js was setting the books_attributes mannualy.
In controller:
def project_params
params[:project][:books_attributes] = params[:project][:books_or_whatever_name_relationships_have] if params[:project][:books_or_whatever_name_relationships_have]
params.require(:project).permit(:attr1, :attr2,...., books_attributes: [:book_attr1, :book_attr2, ....])
end
So rails checks and filters the nested attributes as it expected them to come
This worked for me. My parent model was an Artist and the child model was a Url.
class ArtistsController < ApplicationController
def update
artist = Artist.find(params[:id].to_i)
artist.update_attributes(artist_params)
render json: artist
end
private
def artist_params
remap_urls(params.permit(:name, :description, urls: [:id, :url, :title, :_destroy]))
end
def remap_urls(hash)
urls = hash[:urls]
return hash unless urls
hash.reject{|k,v| k == 'urls' }.merge(:urls_attributes => urls)
end
end
class Artist < ActiveRecord::Base
has_many :urls, dependent: :destroy
accepts_nested_attributes_for :urls, allow_destroy: true
end
class Url < ActiveRecord::Base
belongs_to :artist
end
... and in coffeescript (to handle deletions):
#ArtistCtrl = ($scope, $routeParams, $location, API) ->
$scope.destroyUrls = []
$scope.update = (artist) ->
artist.urls.push({id: id, _destroy: true}) for id in $scope.destroyUrls
artist.$update(redirectToShow, artistError)
$scope.deleteURL = (artist,url) ->
artist.urls.splice(artist.urls.indexOf(url),1)
$scope.destroyUrls.push(url.id)
Something is missing from all of the answers, which is the inputs for fields_for in the form.
The form works if you do this:
f.fields_for #model.submodel do ..
However, the form is sent as model[submodel], but that's what causes the error others have mentioned in their answers. If you try to do model.update(model_params), Rails will raise an error that it's expecting a Submodel type.
To fix this, make sure you follow the :name, value format:
f.fields_for :submodel, #model.submodel do ...
Then in the controller, make sure you put _attributes on your params:
def model_params
params.require(:model).permit(submodel_attributes: [:field])
end
Now the save, update, etc. will work fine.
Wasted several days trying to figure out how to use accepts_nested_attributes with Angular, and the issue is always the same: Rails whitelist will not allow the variables into the params hash. I've tried every single different whitelisting syntax that everyone said on SO and other blogs, tried using :inverse, tried using habtm and mas_many_through, tried manually rolling my own solution but that wont work if the whitelist wont allow params through, tried doing what http://guides.rubyonrails.org says about 'Outside the Scope of Strong Parameters', tried removing whitelisting all together which isnt really an option but it causes other problems anyways. Not sure why rails 4 strong parameter whitelisting wont allow arbitrary data thru, thats a huge problem especially if accepts_nested_attributes doesn't work either.... I guess we are left to just create/delete all associations on a separate page/form/controller and look like an idiot making my end users use several forms/pages to do something that should be easily doable on 1 page with 1 form. Ya know, usually I expect Angular to screw me, but this time Angular worked quite well and it was actually Rails 4 that screwed me twice on 1 issue that should be very straightforward.