JsonConverter:在ReadJson中获取父对象,不带$ refs

时间:2015-06-11 00:13:07

标签: c# json.net

我有一个JSON输入,其对象都是从基类派生的:

public abstract class Base
{
    public Base Parent { get; set; }
}

我尝试使用CustomCreationConverter创建Parent以将每个对象的ReadJson属性设置为JSON输入中的父节点(根节点除外)当然)。这可能吗?我不必在创建后遍历对象来设置Parent属性。

示例时间!

说我有这个输入JSON:

{
  "Name": "Joe",
  "Children": [
    { "Name": "Sam", "FavouriteToy": "Car" },
    { "Name": "Tom", "FavouriteToy": "Gun" },
  ]
}

我有以下两个类:

public class Person
{
    public Person Parent { get; set; }
    public string Name { get; set; }
    public List<Child> Children { get; set; }
}

public class Child : Person
{
    public string FavouriteToy { get; set; }
}

NameFavouriteToy属性反序列化很好,但我希望将任何Parent对象的Person属性设置为,正如您所期望的那样, JSON输入中的实际父对象(可能使用JsonConverter)。到目前为止我能够实现的最好的是在反序列化之后以递归方式遍历每个对象并以此方式设置Parent属性。

P.S。

我想指出,我知道我能够通过JSON内部的引用来做到这一点,但我宁愿避免这种情况。

不重复:(

该问题涉及创建正确派生类的实例,我遇到的问题是在对象的反序列化过程中找到获取上下文的方法。我试图使用JsonConverter的ReadJson方法来设置反序列化对象的属性,以引用同一JSON输入中的另一个对象,不使用使用$ref s

3 个答案:

答案 0 :(得分:2)

我最好的猜测是你在追求这样的事情:

public override bool CanConvert(Type objectType)
{
    return typeof(Person).IsAssignableFrom(objectType);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    object value = Activator.CreateInstance(objectType);
    serializer.Populate(reader, value);

    Person p  = value as Person;
    if (p.Children != null)
    {
        foreach (Child child in p.Children)
        {
            child.Parent = p;
        }
    }
    return value;
}

注意:如果您要对此类进行反序列化(例如在Web应用程序中从http请求反序列化模型),您将获得使用预编译工厂创建对象的更好性能而不是与对象激活器:

object value = serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();

请注意,无法访问父级,因为在子级之后始终会创建父级对象。那就是你需要读取对象所包含的json,以便能够完全构造对象,当你读取对象的最后一个括号时,你已经读过它的所有子节点。当孩子解析时,没有父母可以获得参考。

答案 1 :(得分:1)

我遇到了类似的困境。但是,在我的特定场景中,我确实需要属性 Parentreadonly,或者至少为 private set。因此,@Andrew Savinykh's solution 尽管非常好,但对我来说还不够。因此,我最终结束了将不同方法合并在一起,直到找到可能的“解决方案”或变通方法。


JsonContext

首先,我注意到 JsonSerializer 提供了一个 public readonly Context 属性,该属性可用于在同一个反序列化过程中涉及的实例和转换器之间共享数据。利用这一点,我实现了自己的上下文类,如下所示:

public class JsonContext : Dictionary<string, object>
{
    public void AddUniqueRef(object instance)
    {
        Add(instance.GetType().Name, instance);
    }

    public bool RemoveUniqueRef(object instance)
    {
        return Remove(instance.GetType().Name);
    }

    public T GetUniqueRef<T>()
    {
        return (T)GetUniqueRef(typeof(T));
    }

    public bool TryGetUniqueRef<T>(out T value)
    {
        bool result = TryGetUniqueRef(typeof(T), out object obj);
        value = (T)obj;
        return result;
    }

    public object GetUniqueRef(Type type)
    {
        return this[type.Name];
    }

    public bool TryGetUniqueRef(Type type, out object value)
    {
        return TryGetValue(type.Name, out value);
    }
}

接下来,我需要将我的 JsonContext 的一个实例添加到我的 JsonSerializerSetttings 中:

var settings = new JsonSerializerSettings
    {       
        Context = new StreamingContext(StreamingContextStates.Other, new JsonContext()),
        // More settings here [...]
    };

_serializer = JsonSerializer.CreateDefault(settings);

OnDeserializing / OnDeserialized

我尝试在 OnDeserializingOnDeserialized 回调中使用此上下文,但是,正如@Andrew Savinykh 所说,它们按以下顺序调用:

  1. Child.OnDeserializing
  2. Child.OnDeserialized
  3. Person.OnDeserializing
  4. Person.OnDeserialized

编辑:在完成我的初始实施(参见解决方案 2)后,我注意到,在使用任何类型的 *CreationConverter 时,上述顺序被修改为如下:

  1. Person.OnDeserializing
  2. Child.OnDeserializing
  3. Child.OnDeserialized
  4. Person.OnDeserialized

我不太确定这背后的原因。这可能与 JsonSerializer 通常使用 Deserialize 的事实有关,它在反序列化回调、实例创建和填充之间从下到上包装对象组合树。相比之下,在使用 CustomCreationConverter 时,序列化器将实例化委托给我们的 Create 方法,然后它可能仅以第二个堆叠顺序执行 Populate

如果我们正在寻找更简单的解决方案(请参阅解决方案 1),这种堆叠的回调调用顺序非常方便。利用此版本,我将这种新方法添加在下面的第一位(解决方案 1)和原始的更复杂的方法(解决方案 2)最后。< /p>


解决方案 1. 序列化回调

解决方案 2 相比,这可能是一种更简单、更优雅的方法。然而,它不支持通过构造函数初始化 readonly 成员。如果是这种情况,请参阅解决方案 2

正如我上面所说的,这个实现的一个要求是一个 CustomCreationConverter 以强制以方便的顺序调用回调。例如,我们可以对 PersonConverterPerson 使用以下 Child

public sealed class PersonConverter : CustomCreationConverter<Person>
{
    /// <inheritdoc />
    public override Person Create(Type objectType)
    {
        return (Person)Activator.CreateInstance(objectType);
    }
}

然后,我们只需在序列化回调中访问我们的 JsonContext 即可共享 Person Parent 属性。

public class Person
{
    public Person Parent { get; set; }
    public string Name { get; set; }
    public List<Child> Children { get; set; }
    
    [OnDeserializing]
    private void OnDeserializing(StreamingContext context)
    {
        ((JsonContext)context.Context).AddUniqueRef(this);
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        ((JsonContext)context.Context).RemoveUniqueRef(this);
    }
}

public class Child : Person
{
    public string FavouriteToy { get; set; }
   
    [OnDeserializing]
    private void OnDeserializing(StreamingContext context)
    {
        Parent = ((JsonContext)context.Context).GetUniqueRef<Person>();
    }
}

解决方案 2. JObjectCreationConverter

这是我最初的解决方案。它确实支持通过参数化构造函数初始化 readonly 成员。它可以与解决方案 1 结合使用,将 JsonContext 的用法移至序列化回调。

在我的特定场景中,Person 类缺少无参数构造函数,因为它需要初始化一些 readonly 成员(即 Parent)。为了实现这一点,我们需要我们自己的 JsonConverter 类,完全基于 CustomCreationConverter 实现,使用带有两个新参数的 abstract T Create 方法:JsonSerializer,以便提供对我的 JsonContextJObject,从 reader 预读一些值。

/// <summary>
///     Creates a custom object.
/// </summary>
/// <typeparam name="T">The object type to convert.</typeparam>
public abstract class JObjectCreationConverter<T> : JsonConverter
{
    #region Public Overrides JsonConverter

    /// <summary>
    ///     Gets a value indicating whether this <see cref="JsonConverter" /> can write JSON.
    /// </summary>
    /// <value>
    ///     <c>true</c> if this <see cref="JsonConverter" /> can write JSON; otherwise, <c>false</c>.
    /// </value>
    public override bool CanWrite => false;

    /// <summary>
    ///     Writes the JSON representation of the object.
    /// </summary>
    /// <param name="writer">The <see cref="JsonWriter" /> to write to.</param>
    /// <param name="value">The value.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <exception cref="NotSupportedException">JObjectCreationConverter should only be used while deserializing.</exception>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotSupportedException($"{nameof(JObjectCreationConverter<T>)} should only be used while deserializing.");
    }

    /// <summary>
    ///     Reads the JSON representation of the object.
    /// </summary>
    /// <param name="reader">The <see cref="JsonReader" /> to read from.</param>
    /// <param name="objectType">Type of the object.</param>
    /// <param name="existingValue">The existing value of object being read.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <returns>The object value.</returns>
    /// <exception cref="JsonSerializationException">No object created.</exception>
    /// <exception cref="JsonReaderException"><paramref name="reader" /> is not valid JSON.</exception>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }

        // Load JObject from stream
        JObject jObject = JObject.Load(reader);

        T value = Create(jObject, objectType, serializer);
        if (value == null)
        {
            throw new JsonSerializationException("No object created.");
        }        
      
        using (JsonReader jObjectReader = jObject.CreateReader(reader))
        {
            serializer.Populate(jObjectReader, value);
        }

        return value;
    }

    /// <summary>
    ///     Determines whether this instance can convert the specified object type.
    /// </summary>
    /// <param name="objectType">Type of the object.</param>
    /// <returns>
    ///     <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
    /// </returns>
    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

    #endregion

    #region Protected Methods

    /// <summary>
    ///     Creates an object which will then be populated by the serializer.
    /// </summary>
    /// <param name="jObject"><see cref="JObject" /> instance to browse the JSON object being deserialized</param>
    /// <param name="objectType">Type of the object.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <returns>The created object.</returns>
    protected abstract T Create(JObject jObject, Type objectType, JsonSerializer serializer);

    #endregion
}

注意: CreateReader 是一种自定义扩展方法,它调用默认和无参数的 CreaterReader,然后从原始 reader 导入所有设置。 See @Alain's response 了解更多详情。

最后,如果我们将此解决方案应用于给定(和自定义)示例:

//{
//    "name": "Joe",
//    "children": [
//    {
//        "name": "Sam",
//        "favouriteToy": "Car",
//        "children": []
//    },
//    {
//        "name": "Tom",
//        "favouriteToy": "Gun",
//        "children": []
//    }
//    ]
//}

public class Person 
{
    public              string             Name     { get; }
    [JsonIgnore] public Person             Parent   { get; }
    [JsonIgnore] public IEnumerable<Child> Children => _children;

    public Person(string name, Person parent = null)
    {
        _children = new List<Child>();
        Name      = name;
        Parent    = parent;
    }

    [JsonProperty("children", Order = 10)] private readonly IList<Child> _children;
}

public sealed class Child : Person
{
    public string FavouriteToy { get; set; }

    public Child(Person parent, string name, string favouriteToy = null) : base(name, parent)
    {
        FavouriteToy = favouriteToy;
    }
}

我们只需添加以下 JObjectCreationConverter

public sealed class PersonConverter : JObjectCreationConverter<Person>
{
    #region Public Overrides JObjectCreationConverter<Person>

    /// <inheritdoc />
    /// <exception cref="JsonSerializationException">No object created.</exception>
    /// <exception cref="JsonReaderException"><paramref name="reader" /> is not valid JSON.</exception>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        object result = base.ReadJson(reader, objectType, existingValue, serializer);
        ((JsonContext)serializer.Context.Context).RemoveUniqueRef(result);
        return result;
    }

    #endregion

    #region Protected Overrides JObjectCreationConverter<Person>

    /// <inheritdoc />
    protected override Person Create(JObject jObject, Type objectType, JsonSerializer serializer)
    {
        var person = new Person((string)jObject["name"]);
        ((JsonContext)serializer.Context.Context).AddUniqueRef(person);
        return person;
    }

    public override bool CanConvert(Type objectType)
    {
        // Overridden with a more restrictive condition to avoid this converter from being used by child classes.
        return objectType == typeof(Person);
    }
    #endregion
}

public sealed class ChildConverter : JObjectCreationConverter<Child>
{
    #region Protected Overrides JObjectCreationConverter<Child>

    /// <inheritdoc />
    protected override Child Create(JObject jObject, Type objectType, JsonSerializer serializer)
    {
        var parent = ((JsonContext)serializer.Context.Context).GetUniqueRef<Person>();
        return new Child(parent, (string)jObject["name"]);
    }

    /// <inheritdoc />
    public override bool CanConvert(Type objectType)
    {
        // Overridden with a more restrictive condition.
        return objectType == typeof(Child);
    }

    #endregion
}

奖励跟踪。 ContextCreationConverter

public class ContextCreationConverter : JsonConverter
{
    #region Public Overrides JsonConverter

    /// <summary>
    ///     Gets a value indicating whether this <see cref="JsonConverter" /> can write JSON.
    /// </summary>
    /// <value>
    ///     <c>true</c> if this <see cref="JsonConverter" /> can write JSON; otherwise, <c>false</c>.
    /// </value>
    public override sealed bool CanWrite => false;

    /// <summary>
    ///     Determines whether this instance can convert the specified object type.
    /// </summary>
    /// <param name="objectType">Type of the object.</param>
    /// <returns>
    ///     <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
    /// </returns>
    public override sealed bool CanConvert(Type objectType)
    {
        return false;
    }

    /// <summary>
    ///     Writes the JSON representation of the object.
    /// </summary>
    /// <param name="writer">The <see cref="JsonWriter" /> to write to.</param>
    /// <param name="value">The value.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <exception cref="NotSupportedException">ContextCreationConverter should only be used while deserializing.</exception>
    public override sealed void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotSupportedException($"{nameof(ContextCreationConverter)} should only be used while deserializing.");
    }

    /// <summary>
    ///     Reads the JSON representation of the object.
    /// </summary>
    /// <param name="reader">The <see cref="JsonReader" /> to read from.</param>
    /// <param name="objectType">Type of the object.</param>
    /// <param name="existingValue">The existing value of object being read.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <returns>The object value.</returns>
    /// <exception cref="JsonReaderException"><paramref name="reader" /> is not valid JSON.</exception>
    /// <exception cref="JsonSerializationException">No object created.</exception>
    public override sealed object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }

        // Load JObject from stream
        JObject jObject = JObject.Load(reader);
        object  value   = Create(jObject, objectType, serializer);

        using (JsonReader jObjectReader = jObject.CreateReader(reader))
        {
            serializer.Populate(jObjectReader, value);
        }

        return value;
    }

    #endregion

    #region Protected Methods

    protected virtual object GetCreatorArg(Type type, string name, JObject jObject, JsonSerializer serializer)
    {
        JsonContext context = (JsonContext)serializer.Context.Context;

        if (context.TryGetUniqueRef(type, out object value))
        {
            return value;
        }

        if (context.TryGetValue(name, out value))
        {
            return value;
        }

        if (jObject.TryGetValue(name, StringComparison.InvariantCultureIgnoreCase, out JToken jToken))
        {
            return jToken.ToObject(type, serializer);
        }

        if (type.IsValueType)
        {
            return Activator.CreateInstance(type);
        }

        return null;
    }

    #endregion

    #region Private Methods

    /// <summary>
    ///     Creates a instance of the <paramref name="objectType" />
    /// </summary>
    /// <param name="jObject">
    ///     The JSON Object to read from
    /// </param>
    /// <param name="objectType">
    ///     Type of the object to create.
    /// </param>
    /// <param name="serializer">
    ///     The calling serializer.
    /// </param>
    /// <returns>
    ///     A new instance of the <paramref name="objectType" />
    /// </returns>
    /// <exception cref="JsonSerializationException">
    ///     Could not found a constructor with the expected signature
    /// </exception>
    private object Create(JObject jObject, Type objectType, JsonSerializer serializer)
    {
        JsonObjectContract contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(objectType);

        ObjectConstructor<object> creator = contract.OverrideCreator ?? GetParameterizedConstructor(objectType).Invoke;
        if (creator == null)
        {
            throw new JsonSerializationException($"Could not found a constructor with the expected signature {GetCreatorSignature(contract)}");
        }

        object[] args = GetCreatorArgs(contract.CreatorParameters, jObject, serializer);
        return creator(args);
    }

    private object[] GetCreatorArgs(JsonPropertyCollection parameters, JObject jObject, JsonSerializer serializer)
    {
        var result = new object[parameters.Count];

        for (var i = 0; i < result.Length; ++i)
        {
            result[i] = GetCreatorArg(parameters[i].PropertyType, parameters[i].PropertyName, jObject, serializer);
        }

        return result;
    }

    private ConstructorInfo GetParameterizedConstructor(Type objectType)
    {
        var constructors = objectType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
        return constructors.Length == 1 ? constructors[0] : null;
    }

    private string GetCreatorSignature(JsonObjectContract contract)
    {
        StringBuilder sb = contract.CreatorParameters
                                   .Aggregate(new StringBuilder("("), (s, p) => s.AppendFormat("{0} {1}, ", p.PropertyType.Name, p.PropertyName));
        return sb.Replace(", ", ")", sb.Length - 2, 2).ToString();
    }

    #endregion
}

用法:

// For Person we could use any other CustomCreationConverter.
// The only purpose is to achievee the stacked calling order for serialization callbacks.
[JsonConverter(typeof(ContextCreationConverter))]
public class Person
{
    public              string             Name     { get; }
    [JsonIgnore] public IEnumerable<Child> Children => _children;
    [JsonIgnore] public Person             Parent   { get; }

    public Person(string name, Person parent = null)
    {
        _children = new List<Child>();
        Name      = name;
        Parent    = parent;
    }   

    [OnDeserializing]
    private void OnDeserializing(StreamingContext context)
    {
        ((JsonContext)context.Context).AddUniqueRef(this);
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        ((JsonContext)context.Context).RemoveUniqueRef(this);
    }

    [JsonProperty("children", Order = 10)] private readonly IList<Child> _children;
}

[JsonConverter(typeof(ContextCreationConverter))]
public sealed class Child : Person
{
    [JsonProperty(Order = 5)] public string FavouriteToy { get; set; }

    public Child(Person parent, string name, string favouriteToy = null) : base(name, parent)
    {
        FavouriteToy = favouriteToy;
    }
}

答案 2 :(得分:0)

我创建了一个例子:

        public class Base
        {
            //This is JSON object's attribute.
            public string CPU {get; set;}
            public string PSU { get; set; }
            public List<string> Drives { get; set; }
            public string price { get; set; }
            //and others...
        }

        public class newBase : Base
        {
            ////same
            //public string CPU { get; set; }
            //public string PSU { get; set; }
            //public List<string> Drives { get; set; }

            //convert to new type
            public decimal price { get; set; }  //to other type you want

            //Added new item
            public string from { get; set; }
        }

    public class ConvertBase : CustomCreationConverter<Base>
    {
        public override Base Create(Type objectType)
        {
            return new newBase();
        }
    }

    static void Main(string[] args)
    {
        //from http://www.newtonsoft.com/json/help/html/ReadJsonWithJsonTextReader.htm (creadit) + modify by me
        string SimulateJsonInput = @"{'CPU': 'Intel', 'PSU': '500W', 'Drives': ['DVD read/writer', '500 gigabyte hard drive','200 gigabype hard drive'], 'price' : '3000', 'from': 'Asus'}";

        JsonSerializer serializer = new JsonSerializer();
        Base Object = JsonConvert.DeserializeObject<Base>(SimulateJsonInput);
        Base converted = JsonConvert.DeserializeObject<Base>(SimulateJsonInput, new ConvertBase());
        newBase newObject = (newBase)converted;

        //Console.Write(Object.from);
        Console.WriteLine("Newly converted atrribute type attribute =" + " " + newObject.price.GetType());
        Console.WriteLine("Newly added attribute =" + " " + newObject.from);
        Console.Read();   
    }
希望这会有所帮助。 支持链接:Json .net documentation