Home > database >  Rails JSON.parse "unexpected token" error
Rails JSON.parse "unexpected token" error

Time:07-08

I have a simple controller that is hit by webhooks. I need to store all data sent in a model's metadata which is a text column for later consumption.

class NotificationsController < ApplicationController
  def create
    notification = Notification.new(
      metadata: params,
    )
    if notification.save
      head :ok
    end
  end
end

When I inspect params.class inside any controller action, I get an ActionController::Parameters object that acts like a hash.

However, when storing params in the metadata as shown above, I get a string that looks like this:

"{\"SmsSid\"=>\"ID\", \"SmsStatus\"=>\"STATUS\",\"controller\"=>\"notifications\", \"action\"=>\"create\"}"

I tried converting that string back to a hash, but doing JSON.parse(params) throws the following error:

JSON::ParserError: 783: unexpected token at "{\"SmsSid\"=>\"ID\", \"SmsStatus\"=>\"STATUS\",\"controller\"=>\"notifications\", \"action\"=>\"create\"}"

Is this because the column type is text and not jsonb? If so, is there any workaround that does not involve a DB migration to change the column type?

CodePudding user response:

When I inspect params.class inside any controller action, I get an ActionController::Parameters object that acts like a hash.

Yes, this is what params is. It's an object, that acts like a hash in most respects.

However, when storing params in the metadata as shown above, I get a string that looks like this:

"{\"SmsSid\"=>\"ID\", \"SmsStatus\"=>\"STATUS\",\"controller\"=>\"notifications\", \"action\"=>\"create\"}"

Yes, because you're asking Rails to take something that is not a string, and convert it to a string so it can be stored in a text column. Rails does this by calling .to_s on the object, which returns the string representation you're seeing here.

I tried converting that string back to a hash, but doing JSON.parse(params) throws the following error:

That string isn't JSON. If you want to serialize the params hash to JSON, you can use the serialization API to define how the column should be serialized, and then save some safe subset of params:

class Notification < ActiveRecord::Base
  serialize :metadata, JSON
end

...

notification = Notification.new(
  metadata: params.permit(:SmsSid, :SmsStatus),
)

Afterwards, you can access notification.params and it will be transparently deserialized for you.

Note that simply dumping the entire unsanitized params object into your database is a great way to allow attackers to flood your database with gigabytes of garbage text. You should never assume that params is safe; only work with a subset of params that you expect, and make sure the associated values conform to your expectations. Simply using params.permit(...) as I've done above is still not sufficient, as the values could be extremely long, or contain arbitrary garbage. You should use validators on your model to enforce length and format restrictions.

CodePudding user response:

I think storing the data as json string can solve the problem. like this:

class NotificationsController < ApplicationController
  def create
    notification = Notification.new(
      metadata: params.to_json,
    )
    if notification.save
      head :ok
    end
  end
end

Then:

JSON.parse(Notification.first.metadata)

or you can override the metadata getter method if don't want to write the above code everywhere

class Notification < ApplicationRecord

  def metadata
    JSON.parse(self[:metadata])
  end
end
  • Related