Использование собственного метода to_json во вложенных объектах

У меня есть структура данных, которая использует класс Set из стандартной библиотеки Ruby. Я хотел бы иметь возможность сериализовать мою структуру данных в строку JSON.

По умолчанию Set сериализуется как массив:

>> s = Set.new [1,2,3]
>> s.to_json
=> "[1,2,3]"

Что хорошо, пока вы не попытаетесь десериализовать его.

Итак, я определил обычай to_json метод:

class Set
  def to_json(*a)
    {
      "json_class" => self.class.name,
      "data" => {
        "elements" => self.to_a
      }
    }.to_json(*a)
  end

  def self.json_create(o)
    new o["data"]["elements"]
  end
end

Который прекрасно работает:

>> s = Set.new [1,2,3]
>> s.to_json
=> "{\"data\":{\"elements\":[1,2,3]},\"json_class\":\"Set\"}"

Пока я не поместил Набор в Хеш или что-то:

>> a = { 'set' => s }
>> a.to_json
=> "{\"set\":[1,2,3]}"

Любая идея, почему мой обычай to_json не вызывается, когда Set вложен в другой объект?

3 ответа

Первый блок предназначен для Rails 3.1 (старые версии будут почти такими же); второй кусок для стандартного не-Rails JSON. Пропустить до конца, если tl;dr.


Ваша проблема в том, что Rails делает это:

[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass|
  klass.class_eval <<-RUBY, __FILE__, __LINE__
    # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
    def to_json(options = nil)
      ActiveSupport::JSON.encode(self, options)
    end
  RUBY
end

в active_support/core_ext/object/to_json.rb, В частности, это меняет Хэша to_json метод просто ActiveSupport::JSON.encode вызов.

Затем, глядя на ActiveSupport::JSON::Encoding::Encoderмы видим это:

def encode(value, use_options = true)
  check_for_circular_references(value) do
    jsonified = use_options ? value.as_json(options_for(value)) : value.as_json
    jsonified.encode_json(self)
  end   
end

Таким образом, вся кодировка Rails JSON проходит as_json, Но вы не определяете свой собственный as_json для Set, вы просто настраиваете to_json и путаюсь, когда Rails игнорирует то, что не использует.

Если вы создали свой собственный Set#as_json:

class Set
    def as_json(options = { })
        {
            "json_class" => self.class.name,
            "data" => { "elements" => self.to_a }
        }
    end
end

тогда вы получите то, что вам нужно, в консоли Rails и Rails в целом:

> require 'set'
> s = Set.new([1,2,3])
> s.to_json
 => "{\"json_class\":\"Set\",\"data\":{\"elements\":[1,2,3]}}"
> h = { :set => s }
> h.to_json
 => "{\"set\":{\"json_class\":\"Set\",\"data\":{\"elements\":[1,2,3]}}}" 

Имейте в виду, что as_json используется для подготовки объекта к сериализации JSON, а затем to_json производит фактическую строку JSON. as_json методы обычно возвращают простые сериализуемые структуры данных, такие как Hash и Array, и имеют прямые аналоги в JSON; затем, когда у вас есть что-то, что структурировано как JSON, to_json используется для сериализации в линейную строку JSON.


Когда мы смотрим на стандартную не-Rails библиотеку JSON, мы видим такие вещи:

def to_json(*a)
  as_json.to_json(*a)
end

Обезьяна залатана в основные классы (Символ, Время, Дата, ...). Итак, еще раз, to_json как правило, осуществляется с точки зрения as_json, В этой среде нам необходимо включить стандарт to_json а также выше as_json для набора:

class Set
    def as_json(options = { })
        {
            "json_class" => self.class.name,
            "data" => { "elements" => self.to_a }
        }
    end
    def to_json(*a)
        as_json.to_json(*a)
    end
    def self.json_create(o)
        new o["data"]["elements"]
    end
end

И мы включаем ваш json_create метод класса для декодера. Как только все будет настроено правильно, мы получим irb:

>> s = Set.new([1,2,3])
>> s.as_json
=> {"json_class"=>"Set", "data"=>{"elements"=>[1, 2, 3]}}
>> h = { :set => s }
>> h.to_json
=> "{"set":{"json_class":"Set","data":{"elements":[1,2,3]}}}"

Резюме: если вы в Rails, не беспокойтесь о том, чтобы делать что-либо с to_json, as_json это то, с чем ты хочешь играть. Если вы не в Rails, реализуйте большую часть своей логики в as_json (несмотря на то, что говорится в документации) и добавить стандарт to_json реализация (def to_json(*a);as_json.to_json(*a);end) также.

Вот мой подход к получению to_json метод для пользовательских классов, которые, скорее всего, не будут содержать to_a метод (был удален из Object реализация класса в последнее время)

Здесь есть немного магии, используя self.included в модуле. Вот очень хорошая статья 2006 года о module с методами экземпляра и класса http://blog.jayfields.com/2006/12/ruby-instance-and-class-methods-from.html

Модуль предназначен для включения в любой класс, чтобы обеспечить бесшовную to_json функциональность. Перехватывает attr_accessor метод, а не использует свой собственный, чтобы потребовать минимальных изменений для существующих классов.

module JSONable
  module ClassMethods
    attr_accessor :attributes

    def attr_accessor *attrs
      self.attributes = Array attrs
      super
    end
  end

  def self.included(base)
    base.extend(ClassMethods)
  end

  def as_json options = {}
    serialized = Hash.new
    self.class.attributes.each do |attribute|
      serialized[attribute] = self.public_send attribute
    end
    serialized
  end

  def to_json *a
    as_json.to_json *a
  end
end


class CustomClass
  include JSONable
  attr_accessor :b, :c 

  def initialize b: nil, c: nil
    self.b, self.c = b, c
  end
end

a = CustomClass.new(b: "q", c: 23)
puts JSON.pretty_generate a

{
  "b": "q",
  "c": 23
}

Ища решение той же проблемы, я нашел этот отчет об ошибке на трекере проблем Rails. Кроме того, он был закрыт, я думаю, что это все еще происходит в более ранних версиях. Надеюсь, это может помочь.

https://github.com/rails/rails/issues/576

Другие вопросы по тегам