0

オブジェクトの固定キーに基づいて、ドキュメント内のオブジェクトに使用されるクラスを設定できるように、JSON ドキュメントの解析をカスタマイズしようとしています。

たとえば、「account」という値を持つ「type」のキーを持つ JSON 内の任意の場所にあるオブジェクトは、 のインスタンスを作成する必要がありますAccount

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

値「user」を持つ「type」のキーを持つオブジェクトは、のインスタンスを作成する必要があります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 オブジェクトに変換したいと考えています。おそらくいくつかのアプローチがありますが、1 つのアプローチは、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 に答える