In a Rails 4.1.6 app that functions only as a JSON API,
I have a field "json_data" of type text in a database table called 'cards'.
To my understanding, putting the following code in the Card model...
serialize :json_data, JSON
...makes strings stored in this json_data field output properly as JSON (and not a string with escaped quotes) whenever I return the json_data to some requesting client, like when I do
json.cards #cards do |card|
json.id card.id
json.title card.title
json.json_data card.json_data
end
in a index.json.jbuilder view file, for example.
What is confusing is that I am sending POST and PUT requests of this format to create or update cards:
{
"card":{"title":"card 124","json_data":{"text": "hi"}}
}
But json_data is empty each time. A validation for presence of json_data fails!
If, instead, I send the following:
{
"card":{"title":"card 124","json_data":"{\"text\": \"hi\"}"}
}
json_data is interpreted correctly, but the data is stored in the database as json_data: "{\"text\":\"hi\"}", with the backslashes, which I think is undesirable.
When looking around the database with rails console, shouldn't json_data be presented like this?
#<Card id: 1, title: "Card 1", json_data: {"text"=>"Lorem ipsum figaro ci yei."}, beacon_owner_id: 1, template_id: 1, created_at: "2015-03-04 19:56:36", updated_at: "2015-03-04 19:56:36">, #<Card id: 2, title: "Card 2", json_data: {"text"=>"Pre va ipsum figaro ci yei.", "image_link"=>"http://i.imgur.com/sNRv0Jq.png"}, beacon_owner_id: 1, template_id: 2, created_at: "2015-03-04 19:56:36", updated_at: "2015-03-04 19:56:36">
And not like this?
#<Card id: 4, title: "new card", json_data: "{\"text\": \"hi\"}", beacon_owner_id: 1, template_id: 1, created_at: "2015-03-23 02:51:33", updated_at: "2015-03-23 02:51:33">
What is the best way to accept POST/PUT requests where parameters involve JSON for a specific text column? Does it have to be an escaped string (because as described above, if it's directly just more nested JSON, then Rails' validation for the presence of the field fails)?
==EDIT==
Added the controller:
class API::V1::CardsController < API::BaseController
before_filter :authenticate_user_from_token!
before_action :set_card, only: [:show, :update, :destroy]
def show
respond_with(#card)
end
def index
#cards = current_beacon_owner.cards
respond_with(#cards)
end
def create
#card = Card.new(card_params)
#card.beacon_owner_id = current_beacon_owner.id
#card.save
respond_with(#card)
end
def update
#card.update(card_params)
respond_with(#card)
end
def destroy
#card.destroy
respond_with(#card)
end
private
def set_card
#card = Card.find(params[:id])
end
def card_params
puts '-----------------------------------------'
puts "params: #{params}"
puts '-----------------------------------------'
params.require(:card).permit(:title, :json_data, :template_id)
end
end
The knowledge I was missing was about Rails' strong parameters - we're supposed to whitelist nested parameters: https://github.com/rails/strong_parameters#nested-parameters
My controller's card_params was not allowing the nested parameters, hence the validates-presence-of validation failing. Fixing this lets me store data in the database without all the escaping, which is the right way.
Side note: another solution, if using Rails 4.x and postgres, is to use the :json column type for the field.
Related
in my controller I currently have:
invite = Invite.find_by_token(params[:id])
user = invite.user
json_response({
user: user
})
def json_response(object, status = :ok)
render json: object, status: status
end
Right now, user is returning all user fields. I want to return just (id, email)... I've tried:
user = invite.user.select(:id, :email)
user = invite.user.pluck(:id, :email)
neither works. Ideas?
You can use the method as_json passing attributes you want in the response, like:
user.as_json(only: [:id, :email])
I know this question already has an answer, but there is also a nice gem you could use called active_model_serializers. This lets you specify exactly which properties you want in your JSON output for different models and even let's you include relationships to other models in your response.
Gemfile:
gem 'active_model_serializers', '~> 0.10.0'
Then run bundle install.
You can then create a serializer using the generator command:
rails g serializer user
which will create the serializer in project-root/app/serializers/.
In your serializer, you can whitelist the attributes you would like:
project-root/app/serializers/user_serializer.rb:
class UserSerializer < ActiveModel::Serializer
attributes :id, :email
end
Now any time you return a User object it will only output those two attributes, id and email.
Want to print out related models? Easy. You can just add the relationship in your serializer and it will include those related models in your JSON output.
Pretend a user "has many" posts:
class UserSerializer < ActiveModel::Serializer
attributes :id, :email
has_many :posts
end
Now your JSON outputs should look something like:
{
"id": 1,
"email": "user#example.com",
"posts": [{
id: 1,
title: "My First Post",
body: "This is the post body.",
created_at: "2017-05-18T20:03:14.955Z",
updated_at: "2017-05-18T20:03:14.955Z"
}, {
id: 2,
title: "My Second Post",
body: "This is the post body again.",
created_at: "2017-05-19T20:03:14.955Z",
updated_at: "2017-05-19T20:03:14.955Z"
},
...
]
}
Pretty neat and convenient. And if you want to limit the the posts to only print certain columns as well, all you need to do is create a serializer for posts, specify the attributes, and the output will just work.
def jsontest
#users = User.all.limit(10)
render json: #users
end
yields
{
...
"id": 7,
"name": "Sage Smith",
"email": "example-6#railstutorial.org",
"created_at": "2013-10-17T02:29:15.638Z",
"updated_at": "2013-10-17T02:29:15.638Z",
"password_digest": "$2a$10$taHk3udtWN61Il5I18akj.E90AB1TmdL1BkQBKPk/4eZ7YyizGOli",
"remember_token": "118f807d0773873fb5e4cd3b5d98048aef4f6f59",
"admin": false
...
}
But I would like to omit certain certain fields from this API, so I use pluck
def jsontest
#users = User.all.limit(10).pluck(:id, :name, :email, :created_at) ###
render json: #users
end
but pluck returns an array of only values, when I would like to have each object's attributes accessible by hash key.
[
...
7,
"Sage Smith",
"example-6#railstutorial.org",
"2013-10-17T02:29:15.638Z"
...
]
So how can I effectively pluck the values and their keys?
I realize I could sweep through #users and grab the keys before plucking and recreate the hash, but I'm expecting there to be some convenience method that does exactly what I want.
vee's answer is good, but I have one caveat. select instantiates a User for every row in the result, but pluck does not. That doesn't matter if you are only returning a few objects, but if you are returning large batches (50, 100, etc) you'll pay a significant performance penalty.
I ran into this problem, and I switched back to pluck:
#in user.rb
def self.pluck_to_hash(*keys)
pluck(*keys).map{|pa| Hash[keys.zip(pa)]}
end
#in elsewhere.rb
User.limit(:10).pluck_to_hash(*%i[id name email created_at])
It's ugly, but it gets the hash you want, and fast.
I've updated it to reflect Mike Campbell's comment on Oct 11.
Use select instead of pluck:
def jsontest
#users = User.select('id, name, email, created_at').limit(10)
render json: #users
end
Created a simple pluck_to_hash gem to achieve this.
https://github.com/girishso/pluck_to_hash
Usage example..
Post.limit(2).pluck_to_hash([:id, :title])
#
# [{:id=>213, :title=>"foo"}, {:id=>214, :title=>"bar"}]
#
Post.limit(2).pluck_to_hash(:id)
#
# [{:id=>213}, {:id=>214}]
#
# Or use the shorter alias pluck_h
Post.limit(2).pluck_h(:id)
#
# [{:id=>213}, {:id=>214}]
#
Fastest way to return hash of users with selected columns is use ActiveRecord::Base.connection.select_all method.
sql = User.select('id, name, email, created_at').limit(10).to_sql
#users = ActiveRecord::Base.connection.select_all(sql)
render json: #users
#andrewCone - It should be like this:
User.limit(:10).pluck_to_hash([:id, :name, :email, :created_at])
You can pull the data along with the column name as:
#users = User.all.limit(10)
.pluck(:id, :name, :email, :created_at)
.map {|id, name, email, created_at| { id: id, name: name,
email: email,
created_at: created_at } }
This will pull the data and map it according to how you want it. One advantage of using pluck over select is that you can use joins along with it.
#girishso's gem is great, but my project is in Rails 3. It doesn't work.
This article is helpful to me, or install pluck_all gem to achieve this.
Usage:
User.limit(10).pluck_all(:id, :name, :email, :created_at)
I have the following spec for the controller in simple ActiveRecord search feature:
Spec:
it "returns the records that match the given due date" do
create(:task, due_date: '2013-01-01')
create(:task, due_date: '2014-01-01')
get :search, 'filter' => { due_date: '2013-01-01' }
expect(assigns(:tasks)).to \
eq Task.search('filter' => { due_date: '2013-01-01' })
end
The model and controller are simple:
Model:
def self.search(params)
result = self.all #I know this is a bad idea, but starting simple.
params.each do |field, criteria|
if field.match(/due_date|completed_date/) && criteria != nil
result = result.where("DATE(#{field}) = ?", criteria)
end
end
result
end
Controller action:
def search
#tasks = Task.search(params['filter'])
#output from when the spec runs below
#puts params -> {"filter"=>{"due_date"=>"2013-01-01"}, \
# "controller"=>"tasks", \
# "action"=>"search"}
#puts params['filter] -> {"due_date"=>"2013-01-01"}
#puts #tasks.inspect -> just the one record
render 'index'
end
The spec fails, but it appears that it fails because the controller is returning both objects, while Task.search(...) is returning only the object with the specified value for due_date, as expected.
Here is the error message (edited for length):
2) TasksController GET #search returns the records that
match the given due date
Failure/Error: expect(assigns(:tasks)).to
eq Task.search('filter' => { due_date: '2013-01-01' })
expected: #<ActiveRecord::Relation
[#<Task id: 1,
due_date: "2013-01-01 00:00:00",
completed_date: "2013-12-22 03:57:37">,
#<Task id: 2, due_date: "2014-01-01 00:00:00",
completed_date: "2013-12-22 03:57:37">]>
got: #<ActiveRecord::Relation
[#<Task id: 1,
due_date: "2013-01-01 00:00:00",
completed_date: "2013-12-22 03:57:37">]>
You would assume that since the model apparently works (as evidenced by this result and a separate model spec that passes) that there is something wrong with the controller, but the controller is dead simple. I also have a feature spec incorporating the same controller that submits a form, triggers the search action and looks at the output, and the output only includes the one, correct record.
Am I missing something about how assigns works, making a dumb mistake or other?
It was option B, dumb mistake.
The model method takes the value of the filter element of the params hash as an argument, not the fake params hash I need to send to GET #searchin the line above the expectation. Or more clearly maybe, replace:
expect(assigns(:tasks)).to eq Task.search('filter' => { due_date: '2013-01-01' })
with
expect(assigns(:tasks)).to eq Task.search(due_date: '2013-01-01')'
I make a http put request with following parameters:
{"post"=>{"files"=>{"file1"=>"file_content_1",
"file2"=>"file_content_2"}}, "id"=>"4"}
and i need to permit hash array in my code.
based on manuals I've tried like these:
> params.require(:post).permit(:files) # does not work
> params.require(:post).permit(:files => {}) # does not work, empty hash as result
> params.require(:post).permit! # works, but all params are enabled
How to make it correctly?
UPD1: file1, file2 - are dynamic keys
Rails 5.1+
params.require(:post).permit(:files => {})
Rails 5
params.require(:post).tap do |whitelisted|
whitelisted[:files] = params[:post][:files].permit!
end
Rails 4 and below
params.require(:post).tap do |whitelisted|
whitelisted[:files] = params[:post][:files]
end
In rails 5.1.2, this works now:
params.require(:post).permit(:files => {})
See https://github.com/rails/rails/commit/e86524c0c5a26ceec92895c830d1355ae47a7034
I understand that this is an old post. However, a Google search brought me to this result, and I wanted to share my findings:
Here is an alternative solution that I have found that works (Rails 4):
params = ActionController::Parameters.new({"post"=>{"files"=>{"file1"=>"file_content_1", "file2"=>"file_content_2"}}, "id"=>"4"})
params.require(:post).permit(files: params[:post][:files].keys)
# Returns: {"files"=>{"file1"=>"file_content_1", "file2"=>"file_content_2"}}
The difference between this answer and the accepted answer, is that this solution restricts the parameter to only 1 level of dynamic keys. The accepted answer permits multiple depths.
[Edit] Useful tip from comment
"Oh, and you need to verify that params[:post][.files] exists otherwise keys will fail"
Orlando's answer works, but the resulting parameter set returns false from the permitted? method. Also it's not clear how you would proceed if you were to later have other parameters in the post hash that you want included in the result.
Here's another way
permitted_params = params.require(:post).permit(:other, :parameters)
permitted_params.merge(params[:post][:files])
Here's what we had to do in Rails 5.0.0, hope this helps someone.
files = params[:post].delete(:files) if params[:post][:files]
params.require(:post).permit(:id).tap do |whitelisted|
whitelisted[:files] = files.permit!
end
In my case, there was just one attribute which had dynamic keys,
def post_params
marking_keys = Set.new
params[:post][:marking].keys.collect {|ii| marking_keys.add(ii)}
params.require(:post).permit(:name, marking: marking_keys.to_a)
end
Here is another way to get around this:
def post_params
permit_key_params(params[:post]) do
params.require(:post)
end
end
def permit_key_params(hash)
permitted_params = yield
dynamic_keys = hash.keys
dynamic_keys.each do |key|
values = hash.delete(key)
permitted_params[key] = values if values
end
permitted_params
end
This should work for post: { something: {...}, something_else: {...} }
You can use a temporary variable to build your permitted list like so:
permitted = params.require(:post).permit(:id)
permitted[:post][:files] = params[:post][:files].permit!
Here's a simple way to do it (works for rails 5):
def my_params
data_params = preset_data_params
params.require(:my_stuff).permit(
:some,
:stuff,
data: data_params
)
end
def preset_data_params
return {} unless params[:my_stuff]
return {} unless params[:my_stuff][:data]
params[:my_stuff][:data].keys
end
Send params as array type like name=date[]**strong text**
def user_post
dates = params[:date]
#render json: { 'response' => params }
i = 0
dates.each do |date|
locations = params['location_'+"#{i}"]
user_names = params['user_'+"#{i}"]
currency_rates = params['currency_'+"#{i}"]
flags = params['flag_'+"#{i}"]
j = 0
locations.each do |location|
User.new(user_name: user_names[j], currency_name: flags[j],
currency_rate: currency_rates[j], currency_flag: flags[j], location: location).save
j =+ 1
end
i =+ 1
end
def
I could not get any of the many proposed answers to work (Rails 5) without either:
knowing all the hash keys in advance, or
virtually negating the value of strong parameters by allowing arbitrary params.
I'm using this solution.
It uses the standard strong parameters rig to clean up most of the params,
and the Hash attribute is added back in explicitly.
# Assuming:
class MyObject < ApplicationRecord
serialize :hash_attr as: Hash
#...
end
# MyObjectsController method to filter params:
def my_object_params
# capture the hashed attribute value, as a Hash
hash_attr = params[:my_object] && params[:my_object][:hash_attr] ?
params[my_object][:hash_attr].to_unsafe_h : {}
# clean up the params
safe_params = params.require(:my_object).permit(:attr1, :attr2) # ... etc
# and add the hashed value back in
safe_params.to_unsafe_h.merge hash_attr: hash_attr
end
Let's use a more complicated subset of data:
task: {
code: "Some Task",
enabled: '1',
subtask_attributes: {
'1' => { field: 'something', rules: {length_10: true, phone: false, presence: false }} ,
'2' => { field: 'another', rules: {length_10: true, phone: false, presence: false }}
}
}
So we send it to Strong Parameters for processing:
params = ActionController::Parameters.new({
task: {
code: "Some Task",
enabled: '1',
subtask_attributes: {
'1' => { field: 'something', rules: {length_10: true, phone: false, presence: false }} ,
'2' => { field: 'another', rules: {length_10: true, phone: false, presence: false }}
}
}
})
We will not be able to specify :rules in Strong Params in Rails 4 because it is a hash of data:
permitted = params.require(:task).permit(:code, :enabled, subtask_attributes: [:field, :rules])
Unpermitted parameter: rules
Unpermitted parameter: rules
So what if you want to whitelist specific attributes AND a COLLECTION of hashes of data. The accepted answer does not whitelist specified attributes. You have to do this:
params.require(:task).permit(
:code, :enabled,
subtask_attributes: [:field, :rules],
)
# whitelist the validation rules hash
params.require(:task).tap do |whitelisted|
params[:task][:subtask_attributes].each do |k,v|
whitelisted[:subtask_attributes][k] = params[:task][:subtask_attributes][k]
whitelisted.permit!
end
end
After trying several of the solutions here, none worked. Only aboved worked for nested attributes in a has_many association which contains arbitrary hash data.
I know this is an old post, one of many with different ways to update a serialize hash field. I thought I give my version that I accidently found by piecing together some methods. I'll just use my application. This is Rails 7.0.4 and Ruby 3.0. I also use slim templates.
I have a Taxable model that contains semi-persistent tax rates for different Departments. All items are Sales Tax taxable, but in my case, Liquor adds an additional tax. The Taxable table only has two fields with tax being a serialized JSON field.
create_table "taxables", force: :cascade do |t|
t.date "date"
t.string "tax"
...
end
If a Tax is changed or added, the I would add a new record to reflect the change that took place on some date. Any ticket that had a tax in the past would use the record that is the earliest record before the ticket date. Anything new will the new changed record
The Taxable model has a constant that names all taxes that may be used:
TaxesUsed = %w(sales county federal city liquor)
The records would be something like:
[#<Taxable:0x0000000111c7bfc0
id: 2,
date: Sun, 01 Jan 2023,
tax: {"sales"=>"8.0", "county"=>"2.0", "federal"=>"0.0", "city"=>"0.0", "liquor"=>"3.0"} ...
#<Taxable:0x0000000111c7b980
id: 3,
date: Fri, 01 Jan 2021,
tax: {"sales"=>"8.0", "county"=>"2.0", "federal"=>"0.0", "city"=>"0.0", "liquor"=>"4.0"}...
]
I initially had a kludge that worked, which was creating the hash from some un-permitted parameter and updating the record. I then found mention of using form_with to describe the Tax field and to my surprise it worked! The form:
= form_with(model: #taxable) do |form|
div
= form.label :date, style: "display: block"
= form.date_field :date
div
= form.label :tax, style: "display: block", class:"font-bold"
= form.fields_for :tax do |tax|
# #taxable.tax is the existing serialize tax hash or a new default hash
- #taxable.tax.each do |k,v|
div.flex.gap-2
div.w-36.font-bold.text-right = k
div
= tax.text_field k, value:v
div[class="#{btn_submit}"]
= form.submit
I had to define a new taxable_parmam that states that :tax is a Hash
def taxable_params
params.require(:taxable).permit(:date, :tax => {})
end
Submitting the form give me params:
Parameters: {"authenticity_token"=>"[FILTERED]",
"taxable"=>{"date"=>"2021-01-01", "tax"=>{"sales"=>"8.0",
"county"=>"2.0", "federal"=>"0.0", "city"=>"0.0",
"liquor"=>"4.0"}}, "commit"=>"Update Taxable", "id"=>"3"}
and it works! I forgot about form_with but this is about a simple as you can get just using plain ol Rails.
Update: I forgot that stuff coming from form fields is text. I had to get the params to a new hash, change the float values (percents) and update using the new hash
I'm having problems with an insert to the database. First an explanation of my little Blog app.
The models: Users och Posts. http://pastie.org/2694864
A post have columns: title, body, user id
3 controllers:
Session, Application (with current_user) and PostController: http://pastie.org/2695386
My loggin session seems to work but when a logged in user shoult write a post the database doesn't recognize any user_id. It's just set to nil. rails console:
=> #<Post id: 17, title: "hello", body: "hello world", created_at: "2011-10-14 14:54:25", updated_at: "2011-10-14 14:54:25", user_id: nil>
I guess it's in the post controller line 88 this should be fixed but I can't figure it out.
I have also tried:
#post = Post.new(params[:post], :user_id => session[:user_id])
But the user_id stills sets to nil!
This is my first app so I would be really greatful for detaild answears.
Tanx!
The problem is that you're passing Post.new two arguments (two hashes in this case), but it only takes one argument. Try this:
#post = Post.new(params[:post].merge!(:user_id => session[:user_id]))