web-dev-qa-db-ja.com

カスタムJsonConverter WriteJsonはサブプロパティのシリアル化を変更しません

私はいつも、JSONシリアライザーが実際にオブジェクトのツリー全体をトラバースし、カスタムのJsonConverterのWriteJson関数を、それが遭遇するインターフェース型のオブジェクトごとに実行するという印象を持っていました-そうではありません。

次のクラスとインターフェイスがあります。

public interface IAnimal
{
    string Name { get; set; }
    string Speak();
    List<IAnimal> Children { get; set; }
}

public class Cat : IAnimal
{
    public string Name { get; set; }
    public List<IAnimal> Children { get; set; }        

    public Cat()
    {
        Children = new List<IAnimal>();
    }

    public Cat(string name="") : this()
    {
        Name = name;
    }

    public string Speak()
    {
        return "Meow";
    }       
}

 public class Dog : IAnimal
 {
    public string Name { get; set; }
    public List<IAnimal> Children { get; set; }

    public Dog()
    {
        Children = new List<IAnimal>();   
    }

    public Dog(string name="") : this()
    {
        Name = name;
    }

    public string Speak()
    {
        return "Arf";
    }

}

JSONの$ typeプロパティを回避するために、カスタムJsonConverterクラスを作成しました。そのクラスのWriteJsonは

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    JToken t = JToken.FromObject(value);

    if (t.Type != JTokenType.Object)
    {
        t.WriteTo(writer);                
    }
    else
    {
        IAnimal animal = value as IAnimal;
        JObject o = (JObject)t;

        if (animal != null)
        {
            if (animal is Dog)
            {
                o.AddFirst(new JProperty("type", "Dog"));
                //o.Find
            }
            else if (animal is Cat)
            {
                o.AddFirst(new JProperty("type", "Cat"));
            }

            foreach(IAnimal childAnimal in animal.Children)
            {
                // ???
            }

            o.WriteTo(writer);
        }
    }
}

この例では、はい、犬は子供のために猫を飼うことができ、逆もまた同様です。コンバーターで、 "type"プロパティを挿入して、シリアル化に保存できるようにします。次の設定があります。 (ZooにはIAnimalsの名前とリストしかありません。簡潔さと遅延のためにここには含めませんでした;))

Zoo hardcodedZoo = new Zoo()
            {   Name = "My Zoo",               
                Animals = new List<IAnimal> { new Dog("Ruff"), new Cat("Cleo"),
                    new Dog("Rover"){
                        Children = new List<IAnimal>{ new Dog("Fido"), new Dog("Fluffy")}
                    } }
            };

            JsonSerializerSettings settings = new JsonSerializerSettings(){
                ContractResolver = new CamelCasePropertyNamesContractResolver() ,                    
                Formatting = Formatting.Indented
            };
            settings.Converters.Add(new AnimalsConverter());            

            string serializedHardCodedZoo = JsonConvert.SerializeObject(hardcodedZoo, settings);

serializedHardCodedZooのシリアル化後の出力は次のとおりです。

{
  "name": "My Zoo",
  "animals": [
    {
      "type": "Dog",
      "Name": "Ruff",
      "Children": []
    },
    {
      "type": "Cat",
      "Name": "Cleo",
      "Children": []
    },
    {
      "type": "Dog",
      "Name": "Rover",
      "Children": [
        {
          "Name": "Fido",
          "Children": []
        },
        {
          "Name": "Fluffy",
          "Children": []
        }
      ]
    }
  ]
}

TypeプロパティはRuff、Cleo、Roverに表示されますが、FidoとFluffyには表示されません。 WriteJsonは再帰的に呼び出されないと思います。そこでそのタイププロパティを取得するにはどうすればよいですか?

余談ですが、なぜ私が期待するようなキャメルケースのIAnimalsがないのですか?

15
Mickael Caruso

コンバーターが子オブジェクトに適用されない理由は、JToken.FromObject()が内部的にシリアライザーの新しいインスタンスを使用するためです。これはコンバーターを認識しません。シリアライザーを渡すことができるオーバーロードがありますが、これを行うと、別の問題が発生します。コンバーター内にいて、JToken.FromObject()を使用して親オブジェクトをシリアル化しようとしているため、無限再帰ループに入ります。 (JToken.FromObject()はシリアライザを呼び出し、シリアライザはコンバータを呼び出し、JToken.FromObject()を呼び出します、など)

この問題を回避するには、親オブジェクトを手動で処理する必要があります。これを行うには、少しのリフレクションを使用して、親プロパティを列挙します。

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    JObject jo = new JObject();
    Type type = value.GetType();
    jo.Add("type", type.Name);

    foreach (PropertyInfo prop in type.GetProperties())
    {
        if (prop.CanRead)
        {
            object propVal = prop.GetValue(value, null);
            if (propVal != null)
            {
                jo.Add(prop.Name, JToken.FromObject(propVal, serializer));
            }
        }
    }
    jo.WriteTo(writer);
}

フィドル: https://dotnetfiddle.net/sVWsE4

16
Brian Rogers

ここにアイデアがあります。すべてのプロパティでリフレクションを実行する代わりに、通常はシリアル化されたJObjectを反復処理し、関心のあるプロパティのトークンを変更します。

そうすれば、すべての '' JsonIgnore ''属性やその他の魅力的な組み込み機能を引き続き活用できます。

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    JToken jToken = JToken.FromObject(value);

    if (jToken.Type == JTokenType.Object)
    {
        JObject jObject = (JObject)jToken;
        ...
        AddRemoveSerializedProperties(jObject, val);
        ...
    }
    ...
}

その後

private void AddRemoveSerializedProperties(JObject jObject, MahMan baseContract)
   {
       jObject.AddFirst(....);

        foreach (KeyValuePair<string, JToken> propertyJToken in jObject)
        {
            if (propertyJToken.Value.Type != JTokenType.Object)
                continue;

            JToken nestedJObject = propertyJToken.Value;
            PropertyInfo clrProperty = baseContract.GetType().GetProperty(propertyJToken.Key);
            MahMan nestedObjectValue = clrProperty.GetValue(baseContract) as MahMan;
            if(nestedObj != null)
                AddRemoveSerializedProperties((JObject)nestedJObject, nestedObjectValue);
        }
    }
1
GettnDer

親と子の型に2つのカスタムコンバーターを使用してこの問題が発生しました。私が見つけたより簡単な方法は、JToken.FromObject()のオーバーロードがserializerをパラメーターとして取るので、WriteJson()で指定したシリアライザーを渡すことができるということです。ただし、再帰的な呼び出しを避けるためにシリアライザからコンバータを削除する必要があります(ただし、後で追加し直します)。

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    serializer.Converters.Remove(this);
    JToken jToken = JToken.FromObject(value, serializer);
    serializer.Converters.Add(this);

    // Perform any necessary conversions on the object returned
}
1
Framnk