Использование собственного метода 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. Кроме того, он был закрыт, я думаю, что это все еще происходит в более ранних версиях. Надеюсь, это может помочь.