0

我正在尝试自定义 JSON 文档的解析,以便我可以根据对象中的固定键设置用于文档中对象的类。

例如,任何出现在 JSON 中任何位置的对象,其键为“type”且值为“account”,都应创建Account.

{"type": "account", "account_id": "1234"}

键为“type”且值为“user”的对象应创建User.

{"type": "user", "username": "jane", "email": "jane@example.com"}

JSON 中的任何其他对象都应该正常解码,并且文档中的任何位置都可能嵌入了多个帐户/用户样式对象。

例如:

{
    "version": "1.0",
    "users": [{"type": "user", "username": "jane", "email": "jane@example.com"}],
    "extra": {"paid": true, "account": {"type": "account", "account_id": "1234"}}
}

在 Python 中,我可以指定一个自定义解码器,允许我控制对象的实例化方式。

class CustomJSONDecoder(json.JSONDecoder):
    def __init__(self, *args, **kwargs):
        kwargs['object_hook'] = self.dict_to_object
        super(CustomJSONDecoder, self).__init__(*args, **kwargs)

    def dict_to_object(self, data):
        identifier = data.get('type')
        if identifier == 'account':
            return Account(data)
        elif identifier == 'user':
            return User(data)
        return data

def loads(content):
    return json.loads(content, cls=CustomJSONDecoder)

同样在Javascript中我可以做到这一点......

function decoder(key, val) {
    if (val._type === "account") {
        return Account(val);
    } else if (val._type === "user") {
        return User(val);
    }
    return val;
}

function loads(content) {
    return JSON.parse(content, decoder);
}

我不确定在 Ruby 中实现相同目标的最简单方法。

我已经看到它JSON.parse需要一个object_class参数,但这是一个固定的类,而不是动态确定的。

我真的不介意最终结果是通过对解析后的 JSON 进行后处理,还是在解析阶段发生。最简单的方法可能是正常解析 JSON,然后遍历并更改生成的数据结构,尽管如果是这种情况,我仍然会很感激有关实现它的一些指导。

4

2 回答 2

3

对后处理的 JSON 进行操作肯定会很容易。String#classify并且String#constantize在这里对于按名称从字符串中获取类很有用。

parsed = JSON.parse(data)
klass = parsed.delete("type").classify.constantize
instance = klass.new(parsed)

使用手动递归

解析 JSON 后,您可以将某些 JSON 结构反序列化为对象,如下所示:

SAFE_TYPES = %w(user account)
def deep_deserialize(data)
  case data
  when Array
    data.map {|value| deep_deserialize(value) }
  when Hash
    deserialized = Hash[*data.flat_map {|k, v| [k, deep_deserialize(v)] }]
    if deserialized.key?("type") && SAFE_TYPES.include?(deserialized["type"])
      klass = deserialized.delete("type").classify.constantize
      klass.new(deserialized)
    else
      deserialized
    end
  else
    data
  end
end

这只是遍历树,每当它找到带有type键的散列时,它都会查看该类型是否可以安全实例化,如果是,则使用给定属性实例化它。

一个小测试:

require 'active_support/all'
require 'rspec'
require 'pp'

class Base
  def initialize(attributes)
    @attributes = attributes
  end
end

class User    < Base; end
class Account < Base; end
class Admin   < Base; end

json = <<-EOF
{
    "version": "1.0",
    "users": [{"type": "user", "username": "jane", "email": "jane@example.com"}],
    "extra": {"paid": true, "account": {"type": "account", "account_id": "1234"}},
    "bogus": {"type": "admin", "password": "0wn3d"}
}
EOF

pp deep_deserialize JSON.parse(json)
describe "deep_deserialize" do
  subject { deep_deserialize JSON.parse(json) }

  it "should deserialize permitted classes" do
    subject["users"][0].should be_a User
  end

  it "should deserialize in nested hashes" do
    subject["extra"]["account"].should be_a Account
  end

  it "should not deserialize non-permitted classes" do
    subject["bogus"].should be_a Hash
    subject["bogus"]["type"].should == "admin"
  end
end

并输出:

{"version"=>"1.0",
 "users"=>
  [#<User:0x000000023e6050
    @attributes={"username"=>"jane", "email"=>"jane@example.com"}>],
 "extra"=>
  {"paid"=>true,
   "account"=>#<Account:0x000000023e5948 @attributes={"account_id"=>"1234"}>},
 "bogus"=>{"type"=>"admin", "password"=>"0wn3d"}}

deep_deserialize
  should deserialize permitted classes
  should deserialize in nested hashes
  should not deserialize non-permitted classes

使用 JSON.load

JSON.load为我们处理递归,所以我们可以使用它。如果将 proc 的返回值用于反序列化值,这会简单得多JSON.load,但它似乎没有这样做,所以我们只剩下内联替换。

def deserialize_obj(obj, safe_types = %w(user account))
  type = obj.is_a?(Hash) && obj["type"]
  safe_types.include?(type) ? type.classify.constantize.new(obj) : obj
end

JSON.load(json, proc {|obj|
  case obj
  when Hash
    obj.each {|k, v| obj[k] = deserialize_obj v }
  when Array
    obj.map! {|v| deserialize_obj v }
  end
})
于 2013-07-23T20:47:17.003 回答
0

纯 Ruby 和少量元编程

我不确定我是否完全理解您的问题,但我认为您想要获取一个 JSON 对象并将其转换为某种可自定义的 Ruby 对象。可能有多种方法,但一种方法是将 JSON 对象转换为Struct,然后您可以对其进行修改或传递给另一个对象。例如,使用 Ruby 2.0:

require 'json'

# Use some metaprogramming to define a Struct based on the value of the
# "type" key.
def json_to_struct string
    hash  = JSON.load string 
    klass = hash['type'].capitalize
    hash.delete 'type'
    Class.new Struct.new(klass, *hash.keys)
    Object.const_get("Struct::#{Struct.constants.last}").new *hash.values
end 

my_struct = json_to_struct '{"type": "account", "account_id": "1234"}'
# => #<struct Struct::Account account_id="1234">

my_struct.class.to_s.split('::').last
# => "Account"

请注意,这里的一些魔法依赖于 MRI 2.0 提供的有序哈希。如果您使用的其他解释器未按构建 Struct 所需的顺序提供密钥,则您可能需要改用OpenStruct

太好了,一个结构……现在呢?

有了 Struct 后,您可以向该类添加其他方法或对其值进行操作,以以任何适合您的方式自定义数据。例如,您可以直接修改数据:

# Operate directly on a Struct value. Subtract 1,034 from the account ID
# and save the new value back into the Struct.
my_struct['account_id'] = my_struct['account_id'].to_i - 1_034
# => 200

# The new value is now stored in the Struct.
my_struct
# => #<struct Struct::Account account_id=200>

或者,您可以使用单例方法向 Struct 类添加行为。例如:

# Add a singleton method to your Struct.
def my_struct.subtract number
  self["account_id"] = account_id - number
end

my_struct.subtract 10
# => 190

my_struct
# => #<struct Struct::Account account_id=190>
于 2013-07-23T23:59:08.430 回答