57

I have a model that uses a serialized column:

class Form < ActiveRecord::Base
  serialize :options, Hash
end

Is there a way to make this serialization use JSON instead of YAML?

Marc-André Lafortune
  • 78,216
  • 16
  • 166
  • 166
Toby Hede
  • 36,755
  • 28
  • 133
  • 162
  • I was hoping there would be some "magic" or plugin, but the solution is quite straight forward and works very nicely. – Toby Hede Jan 19 '10 at 00:40
  • toby , can you change the accepted answer to this one? http://stackoverflow.com/a/6971678/190135 see http://meta.stackexchange.com/questions/120568/is-it-possible-to-change-the-chosen-answer – AlexChaffee Aug 17 '16 at 17:08
  • 1
    If you come here wondering why your store accessor won't stop using YAML over JSON, make sure you set the `coder` option to `'JSON'`: https://api.rubyonrails.org/classes/ActiveRecord/Store.html – Dennis Hackethal Nov 26 '22 at 20:41

8 Answers8

160

In Rails 3.1 you can just

class Form < ActiveRecord::Base
  serialize :column, JSON
end
starball
  • 20,030
  • 7
  • 43
  • 238
Justas L.
  • 1,773
  • 2
  • 11
  • 8
  • 1
    I think this is because `[:dump, :load].all {|s| ::JSON.respond_to? s}`, just like the default `YAMLColumn` does. In a controller, `JSON == ::JSON` and `JSON != ::ActiveSupport::JSON`. – Benjamin Atkin Nov 07 '11 at 20:06
  • 1
    This worked up through Rails 3.2.1, but stopped with 3.2.2: see https://github.com/rails/rails/issues/5797 Hopefully that gets fixed, because it's very convenient! – gmcnaughton Apr 10 '12 at 02:40
  • 4
    To clarify what @BenAtkin said, if the Class you pass in to the serialize call responds to `.dump(obj)` and `.load(obj)` it will be used as the coder/serializer. Otherwise the default YAMLColumn class is used as the coder/serializer. The dump and load methods can also be implemented directly in a Model Class, you don't *have* to use a dedicated separate class to do the coding. – jshkol Oct 22 '12 at 23:40
  • This can cause the error: `NoMethodError: undefined method \`read' for nil:NilClass` – Daniël W. Crompton Mar 30 '14 at 21:35
  • So, this realy faster then standart YAML (ar4.1.4) – Ivan Black Jul 21 '14 at 08:59
  • This worked for me. I needed this cuz I was migrating to rails and the db already had data in JSON. This should be the accepted answer – Karthik T Oct 25 '14 at 03:06
58

In Rails 3.1 you can use custom coders with serialize.

class ColorCoder
  # Called to deserialize data to ruby object.
  def load(data)
  end

  # Called to convert from ruby object to serialized data.
  def dump(obj)
  end
end

class Fruits < ActiveRecord::Base
  serialize :color, ColorCoder.new
end

Hope this helps.

References:

Definition of serialize: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/base.rb#L556

The default YAML coder that ships with rails: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/coders/yaml_column.rb

And this is where the call to the load happens: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_methods/read.rb#L132

nocache
  • 1,805
  • 16
  • 18
balu
  • 3,619
  • 1
  • 25
  • 18
  • 1
    Though it hasn't been incorporated into Rails, at time of writing, a JSONColumn Coder was contributed to Rails here: https://gist.github.com/txus/rails/blob/1a2ce8e56c4313a7466c2b0b08a037e44d033f6c/activerecord/lib/active_record/coders/json_column.rb - the pull request details are here: https://gist.github.com/rails/rails/pull/196 . Hope this helps anyone wanting to use serialize + JSON. – Eliot Sykes Sep 24 '11 at 15:14
11

Update

See mid's high rated answer below for a much more appropriate Rails >= 3.1 answer. This is a great answer for Rails < 3.1.

Probably this is what you're looking for.

Form.find(:first).to_json

Update

1) Install 'json' gem:

gem install json

2) Create JsonWrapper class

# lib/json_wrapper.rb

require 'json'
class JsonWrapper
  def initialize(attribute)
    @attribute = attribute.to_s
  end

  def before_save(record)
    record.send("#{@attribute}=", JsonWrapper.encrypt(record.send("#{@attribute}")))
  end

  def after_save(record)
    record.send("#{@attribute}=", JsonWrapper.decrypt(record.send("#{@attribute}")))
  end

  def self.encrypt(value)
    value.to_json
  end

  def self.decrypt(value)
    JSON.parse(value) rescue value
  end
end

3) Add model callbacks:

#app/models/user.rb

class User < ActiveRecord::Base
    before_save      JsonWrapper.new( :name )
    after_save       JsonWrapper.new( :name )

    def after_find
      self.name = JsonWrapper.decrypt self.name
    end
end

4) Test it!

User.create :name => {"a"=>"b", "c"=>["d", "e"]}

PS:

It's not quite DRY, but I did my best. If anyone can fix after_find in User model, it'll be great.

Kyle Heironimus
  • 7,741
  • 7
  • 39
  • 51
St.Woland
  • 5,357
  • 30
  • 30
  • 1
    You don't get that. He wants :options attribute to be serialized into json data, not into yaml. – Eimantas Jan 17 '10 at 08:20
  • Ja, using serialize, the values in the options field get stored in the database as a YAML-serialized value. I want that to be JSON instead. – Toby Hede Jan 17 '10 at 09:45
  • 3
    "Encrypt/decrypt" are misleading names, since no cryptography is involved. Should be "encode/decode." Moot now anyway with Rails 3.1 solution mentioned below. – Paul Cantrell May 18 '12 at 15:30
8

My requirements didn't need a lot of code re-use at this stage, so my distilled code is a variation on the above answer:

  require "json/ext"

  before_save :json_serialize  
  after_save  :json_deserialize


  def json_serialize    
    self.options = self.options.to_json
  end

  def json_deserialize    
    self.options = JSON.parse(options)
  end

  def after_find 
    json_deserialize        
  end  

Cheers, quite easy in the end!

Toby Hede
  • 36,755
  • 28
  • 133
  • 162
  • This will only work with certain data types. For example `input = 'foo'; serialized = input.to_json; JSON.parse(serialized)` #=> JSON::ParserError: 757: unexpected token at '"foo"' from /Users/username/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/json-1.8.3/lib/json/common.rb:155:in `parse' https://makandracards.com/makandra/15611-how-to-fix-unexpected-token-error-for-json-parse A good workaround for this is to use Ruby's Marshal library. It's faster and more reliable than JSON or YAML. – Scott Schupbach Nov 23 '16 at 23:56
3

The serialize :attr, JSON using composed_of method works like this:

  composed_of :auth,
              :class_name => 'ActiveSupport::JSON',
              :mapping => %w(url to_json),
              :constructor => Proc.new { |url| ActiveSupport::JSON.decode(url) }

where url is the attribute to be serialized using json and auth is the new method available on your model that saves its value in json format to the url attribute. (not fully tested yet but seems to be working)

bbonamin
  • 30,042
  • 7
  • 40
  • 49
caribu
  • 31
  • 1
3

I wrote my own YAML coder, that takes a default. Here is the class:

class JSONColumn
  def initialize(default={})
    @default = default
  end
 
  # this might be the database default and we should plan for empty strings or nils
  def load(s)
    s.present? ? JSON.load(s) : @default.clone
  end
     
  # this should only be nil or an object that serializes to JSON (like a hash or array)
  def dump(o)
    JSON.dump(o || @default)
  end
end

Since load and dump are instance methods it requires an instance to be passed as the second argument to serialize in the model definition. Here's an example of it:

class Person < ActiveRecord::Base
  validate :name, :pets, :presence => true
  serialize :pets, JSONColumn.new([])
end

I tried creating a new instance, loading an instance, and dumping an instance in IRB, and it all seemed to work properly. I wrote a blog post about it, too.

danini
  • 365
  • 5
  • 9
Benjamin Atkin
  • 14,071
  • 7
  • 61
  • 60
  • I'm really new to Rails. Where would I put the JSONColumn class? Where would I include it? Thanks! – Venkat D. Apr 10 '12 at 17:26
  • There are many ways to do it, but here's one way: you can put it in `lib/json_column.rb` and add `require 'json_column'` to `config/environment.rb`. – Benjamin Atkin Apr 10 '12 at 22:23
  • Thanks for this. It's nice that you can set a default value. I used this to create a column that acts like a set (unordered array): serialize :something, JsonSerializer.new(Set.new) – Tyler Rick Aug 31 '12 at 00:32
  • @Venkat D., if you name the class so that it matches the filename, then you don't even need to explicitly require the file, because it will be autoloaded when you first reference the constant. I called it JsonSerializer and put it in lib/json_serializer.rb, so I don't even need to require 'json_serializer'. (Just make sure lib/ is in your autoload_paths in config/application.rb: config.autoload_paths = %W( #{config.root}/lib )) – Tyler Rick Aug 31 '12 at 00:36
  • @TylerRick that might be the best answer yet. Feel free to write a new answer based on mine. No need to give me attribution (or rather you can give it as simply and weakly as you like). I hereby release my answer and the blog post it links to into public domain. – Benjamin Atkin Sep 02 '12 at 19:26
  • best answer for me, for some reason the serializing w/ blobs+json couldn't initialize the json in Rails4rc1, this lets me do that. Someone logged an issue https://github.com/rails/rails/pull/10830#issuecomment-18883754 – James Jun 06 '13 at 17:00
1

A simpler solution is to use composed_of as described in this blog post by Michael Rykov. I like this solution because it requires the use of fewer callbacks.

Here is the gist of it:

composed_of :settings, :class_name => 'Settings', :mapping => %w(settings to_json),
                       :constructor => Settings.method(:from_json),
                       :converter   => Settings.method(:from_json)

after_validation do |u|
  u.settings = u.settings if u.settings.dirty? # Force to serialize
end
Aleran
  • 9
  • 1
  • Unfortunately, composed_of has been deprecated. http://blog.plataformatec.com.br/2012/06/about-the-composed_of-removal/ recommends using a custom serializer instead. – Tyler Rick Aug 30 '12 at 23:23
0

Aleran, have you used this method with Rails 3? I've somewhat got the same issue and I was heading towards serialized when I ran into this post by Michael Rykov, but commenting on his blog is not possible, or at least on that post. To my understanding he is saying that you do not need to define Settings class, however when I try this it keeps telling me that Setting is not defined. So I was just wondering if you have used it and what more should have been described? Thanks.

Andrew Lank
  • 1,607
  • 1
  • 15
  • 29
  • Of course you need the Setting class. But you could probably directly work on the JSON class. :constructor => JSON.method(:parse), :converter => JSON.method(:parse), :mapping => %w(my_attribute to_json). Didn't try that though. – balu May 12 '11 at 16:04