Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why render json: is returning NULL instead of boolean value

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.

More information:

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

PostgreSQL

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?

like image 312
radubogdan Avatar asked Mar 16 '23 16:03

radubogdan


2 Answers

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.

like image 176
Drenmi Avatar answered Apr 26 '23 07:04

Drenmi


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
like image 31
johnsorrentino Avatar answered Apr 26 '23 06:04

johnsorrentino