In JSON.NET how to get a reference to every deserialized object?

不问归期 提交于 2019-12-24 08:02:52

问题


I'm attempting to implement IDeserializationCallback using JSON.NET. I'm deserializing an object, and I would like to generate a list of all the objects which were deserialized which implement IDeserializationCallback, what would be the best way to do this? Does JSON.NET have any appropriate extension point to facilitate this? I have a (seemingly) working solution below, however it is quite ugly, so I'm convinced there must be a better way to do this. Any help is appreciated, thanks!

    private static JsonSerializer serializer = new JsonSerializer();

    static cctor()
    {
        serializer.Converters.Add(new DeserializationCallbackConverter());
    }

    public static T Deserialize<T>(byte[] data)
    {
        using (var reader = new JsonTextReader(new StreamReader(new MemoryStream(data))))
        using (DeserializationCallbackConverter.NewDeserializationCallbackBlock(reader))
            return serializer.Deserialize<T>(reader);
    }

    private class DeserializationCallbackConverter : JsonConverter
    {
        [ThreadStatic]
        private static ScopedConverter currentConverter;

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            return currentConverter.ReadJson(reader, objectType, serializer);
        }

        public override bool CanConvert(Type objectType)
        {
            return currentConverter == null ? false : currentConverter.CanConvert();
        }

        public override bool CanWrite
        {
            get { return false; }
        }

        public static IDisposable NewDeserializationCallbackBlock(JsonReader reader)
        {
            return new ScopedConverter(reader);
        }

        private class ScopedConverter : IDisposable
        {
            private JsonReader jsonReader;
            private string currentPath;
            private List<IDeserializationCallback> callbackObjects;

            public ScopedConverter(JsonReader reader)
            {
                jsonReader = reader;
                callbackObjects = new List<IDeserializationCallback>();
                currentConverter = this;
            }

            public object ReadJson(JsonReader reader, Type objectType, JsonSerializer serializer)
            {
                var lastPath = currentPath;
                currentPath = reader.Path;
                var obj = serializer.Deserialize(reader, objectType);
                currentPath = lastPath;

                var dc = obj as IDeserializationCallback;
                if (dc != null && callbackObjects != null)
                    callbackObjects.Add(dc);
                return obj;
            }

            public bool CanConvert()
            {
                return jsonReader.Path != currentPath;
            }

            public void Dispose()
            {
                currentConverter = null;
                foreach (var obj in callbackObjects)
                    obj.OnDeserialization(null);
            }
        }
    }

回答1:


You can create a custom contract resolver that adds an extra, artificial OnDeserialized callback that tracks creation of reference type objects. Here's one example:

public interface IObjectCreationTracker
{
    void Add(object obj);

    ICollection<object> CreatedObjects { get; }
}

public class ReferenceObjectCreationTracker : IObjectCreationTracker
{
    public ReferenceObjectCreationTracker()
    {
        this.CreatedObjects = new HashSet<object>();
    }

    public void Add(object obj)
    {
        if (obj == null)
            return;
        var type = obj.GetType();
        if (type.IsValueType || type == typeof(string))
            return;
        CreatedObjects.Add(obj);
    }

    public ICollection<object> CreatedObjects { get; private set; }
}

public class ObjectCreationTrackerContractResolver : DefaultContractResolver
{
    readonly SerializationCallback callback = (o, context) =>
        {
            var tracker = context.Context as IObjectCreationTracker;
            if (tracker != null)
                tracker.Add(o);
        };

    protected override JsonContract CreateContract(Type objectType)
    {
        var contract = base.CreateContract(objectType);
        contract.OnDeserializedCallbacks.Add(callback);
        return contract;
    }
}

And then use it as follows:

public static class JsonExtensions
{
    public static T DeserializeWithTracking<T>(string json, out ICollection<object> objects)
    {
        var tracker = new ReferenceObjectCreationTracker();
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new ObjectCreationTrackerContractResolver(),
            Context = new StreamingContext(StreamingContextStates.All, tracker),
            // Add other settings as required.  
            TypeNameHandling = TypeNameHandling.Auto, 
        };
        var obj = (T)JsonConvert.DeserializeObject<T>(json, settings);
        objects = tracker.CreatedObjects;
        return obj;
    }
}

Note that this only returns instances of non-string reference types. Returning instances of value types is more problematic as there is no obvious way to distinguish between a value type that eventually gets embedded into a larger object via a property setter and one that is retained in the object graph as a boxed reference, e.g. as shown in this question. If the boxed value type eventually gets embedded in some larger object there is no way to retain a direct reference to it.

Also note the use of StreamingContext.Context to pass the tracker down into the callback.

You may want to cache the contract resolver for best performance.

Update

In answer to the updated question of how to implement IDeserializationCallback with Json.NET, the above should work for reference types. For value types that implement this interface, you could:

  1. Call the method immediately in the OnDeserialized callback rather than deferring it until serialization is complete, or

  2. Throw an exception indicating that IDeserializationCallback is not supported for structs.



来源:https://stackoverflow.com/questions/39300005/in-json-net-how-to-get-a-reference-to-every-deserialized-object

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!