Why I'm getting null
instead of true/false for is_read when calling resource from API?
Offquestion: Could be related to render json:
internals?
This is the first time when I see this sorcery so please bear with me. Looking for good answer :)
$ curl -X GET -H localhost:3000/api/v1/alerts/1/show | python -m json.tool
{
"body": "Deserunt laboriosam quod consequuntur est dolor cum molestias.",
"created_at": "2015-03-22T15:02:01.927Z",
"id": 1,
"is_read": null,
"subtitle": "Aspernatur non voluptatem minus qui laudantium molestiae.",
"title": "Et nemo magni autem similique consequuntur.",
"updated_at": "2015-03-22T15:02:01.927Z"
}
-i
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
Etag: "3232ec2058ffb13db1f9244366fe2ea8"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: c520cabe-6334-4997-956b-d1f21724b80c
X-Runtime: 0.016337
Server: WEBrick/1.3.1 (Ruby/2.1.2/2014-05-08)
Date: Sun, 22 Mar 2015 15:28:21 GMT
Content-Length: 300
Connection: Keep-Alive
Rails console:
Alert.find(1)
Alert Load (1.1ms) SELECT "alerts".* FROM "alerts" WHERE "alerts"."id" = $1 LIMIT 1 [["id", 1]]
=> #<Alert id: 1, title: "Et nemo magni autem similique consequuntur.", subtitle: "Aspernatur non voluptatem minus qui laudantium mol...", body: "Deserunt laboriosam quod consequuntur est dolor cu...", is_read: false, created_at: "2015-03-22 15:02:01", updated_at: "2015-03-22 15:02:01">
And weird thing happens when I call #to_json
Alert.find(1).to_json
Alert Load (4.1ms) SELECT "alerts".* FROM "alerts" WHERE "alerts"."id" = $1 LIMIT 1 [["id", 1]]
=> "{\"id\":1,\"title\":\"Et nemo magni autem similique consequuntur.\",\"subtitle\":\"Aspernatur non voluptatem minus qui laudantium molestiae.\",\"body\":\"Deserunt laboriosam quod consequuntur est dolor cum molestias.\",\"is_read\":null,\"created_at\":\"2015-03-22T15:02:01.927Z\",\"updated_at\":\"2015-03-22T15:02:01.927Z\"}"
It's also happening when is_read is true.
Rails 4.1.7, Ruby 2.1.2, Postgresql
Alert
create_table "alerts", force: true do |t|
t.string "title"
t.string "subtitle"
t.text "body"
t.boolean "is_read", default: false, null: false
t.datetime "created_at"
t.datetime "updated_at"
end
Script used to populate db:
Alert.populate 100 do |alert|
alert.title = Faker::Lorem.sentence(3)
alert.subtitle = Faker::Lorem.sentence(3)
alert.body = Faker::Lorem.sentence(3)
alert.is_read = false
end
Controller:
def show
alert = Alert.find(params[:id])
render json: alert
end
EDIT 26 Mar 2015
irb(main):013:0> Alert.find(1)
Alert Load (0.6ms) SELECT "alerts".* FROM "alerts" WHERE "alerts"."id" = $1 LIMIT 1 [["id", 1]]
=> #<Alert id: 1, title: "Test", subtitle: "waaat?", body: "heeeelp", is_read: false, created_at: "2015-03-26 15:49:32", updated_at: "2015-03-26 15:56:51">
irb(main):014:0> a.body
=> "heeeelp"
irb(main):015:0> a.title
=> "Test"
irb(main):016:0> a.is_read
=> nil
irb(main):017:0> a["is_read"]
=> false
irb(main):018:0> a.update_attributes(is_read: true)
(0.4ms) BEGIN
SQL (0.5ms) UPDATE "alerts" SET "is_read" = $1, "updated_at" = $2 WHERE "alerts"."id" = 1 [["is_read", "t"], ["updated_at", "2015-03-26 15:57:26.645210"]]
(2.1ms) COMMIT
=> true
irb(main):019:0> a["is_read"]
=> true
Final: it was because there was attr_reader :is_read in the model. Removing that it will result in a correct serialization. Can someone explain this?
Final: it was because there was attr_reader :is_read in the model. Removing that it will result in a correct serialization. Can someone explain this?
ActiveRecord helps map your database fields to methods on your Ruby class by creating a getter method for each attribute, that pulls the value from the internal attribute store (variable.)
When you define attr_reader :is_read
, which is actually shorthand for:
# app/mode/alert.rb
def is_read
@is_read
end
Your getter method that was handily provided by ActiveRecord::Base
will be masked by this newly defined method. This can be unexpected, and definitely seem like sorcery at first.
The behaviour of :attr_reader
is (somewhat sparsely) documented here:
Creates instance variables and corresponding methods that return the value of each instance variable.
http://ruby-doc.org/core-1.9.3/Module.html#method-i-attr_reader
But why then it is coming as null? Could be related to render json: internals?
There are two parts to this question.
Firstly, why is the value not there? The answer to this was given above. Your model has masked the ActiveRecord getter method is_read
, that was poiting to ActiveRecord's internal attribute store, with one pointing to an instance variable @is_read
. Because you do not assign @is_read
in your model, nil
is returned.
Secondly, why is it null
and not nil
? The answer to this is, as you hinted at, related to render: json
. In the JSON specification, you have the following allowed values:
string, number, object, array, true, false, null
To produce a valid JSON response, render: json
replaces Ruby's nil
, with JSON's null
.
So is it safe then to say that you never want to use attr_reader
, attr_accessor
et. al. in your models? Not really. These can be used as virtual, or transient attributes, that are lost once the object is destroyed. One should keep in mind, though, the interactions with ActiveRecord, to avoid running into seemingly obscure bugs.
From your edit:
Alert < ActiveRecord::Base
attr_reader :is_read
end
When you define an ActiveRecord model it automatically creates getters and setters for each column in your table. These getters and setters are different than the ones created using attr_accessor, attr_reader, and attr_writer. They access the attributes hash rather than instance variables. Using attr_accessor, attr_reader, or attr_writer will override the getter that ActiveRecord automatically set. If you want 'is_read' to be read only try this:
Alert < ActiveRecord::Base
attr_readonly :is_read
end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With