Search code examples
jsonruby-on-railsrubymodel-view-controller

Rails JSON.parse "unexpected token" error


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?


Solution

  • 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.