Home > Enterprise >  ActiveRecord erroneously escapes JSON string
ActiveRecord erroneously escapes JSON string

Time:12-01

I have a jsonb-type column called 'payloads' for my Tweet model in my Rails 6.1.1 app. I use a store to access various fields on this attribute directly:

class Tweet < ApplicationRecord
  store :payload, accessors: [:lang, :text, :entities], coder: JSON
end

(Note that the part coder: JSON is necessary to change the serialization from the YAML default to JSON – otherwise you end up with YAML in your jsonb column.)

When I create a new tweet, I see that ActiveRecord erroneously escapes the JSON string during the insertion:

Tweet.create payload: {foo: 'bar'}

TRANSACTION (0.7ms)  BEGIN
  Tweet Create (2.0ms)  INSERT INTO "tweets" ("payload", ...) VALUES ($1, ...) RETURNING "id"  [["payload", "\"{\\\"foo\\\":\\\"bar\\\"}\""], ...]

I'm referring to the part "\"{\\\"foo\\\":\\\"bar\\\"}\"". It looks like it's been double escaped. This results in a sort of 'stringception' where a string within a string is stored in the payloads column, rendering postgres unable to perform any searches on fields using the arrow -> syntax. Postgres can't recognize this value as JSON. (Seemingly miraculously, however, Rails is able to deserialize the field properly on read operations.)

Another SO user has illustrated the issue here: https://dbfiddle.uk/gcwTQOUm

CodePudding user response:

When I do not use store, I cannot reproduce the issue:

Tweet.create payload: {foo: 'bar'}
  TRANSACTION (0.2ms)  BEGIN
  Tweet Create (1.6ms)  INSERT INTO "tweets" ("payload", ...) VALUES ($1, ...) RETURNING "id"  [["payload", "{\"foo\":\"bar\"}"], ...]

"{\"foo\":\"bar\"}" is the desired string.

This has led me to believe that I'm using store wrong.

I checked the docs:

NOTE: If you are using structured database data types (e.g. PostgreSQL hstore/json, or MySQL 5.7 json) there is no need for the serialization provided by .store. Simply use .store_accessor instead to generate the accessor methods. Be aware that these columns use a string keyed hash and do not allow access using a symbol.

In other words, because I was already using a jsonb-type column, Rails already knew to serialize the hash – applying another serialization resulted in a double escape.

So, instead of doing:

store :payload, accessors: [:lang, :text, :entities], coder: JSON

I know do:

store_accessor :payload, :lang, :text, :entities

And it works.

  • Related