简体   繁体   中英

How to serialize or deserialize a JSON Object to a certain depth in C#?

I only want the first depth level of an object (I do not want any children). I am willing to use any library available. Most libraries will merely throw an exception when the recursion depth is reached, instead of just ignoring. If this isn't possible, is there a way to ignore serialization of certain members given a certain datatype?

Edit: Let's say I have an object like so:

class MyObject
{
    String name = "Dan";
    int age = 88;
    List<Children> myChildren = ...(lots of children with lots of grandchildren);
}

I want to remove any children (complex types even) to return an object like this:

class MyObject
{
    String name = "Dan";
    int age = 88;
    List<Children> myChildren = null;
}

This is possible in Json.NET using some coordination between the JsonWriter and the serializer's ContractResolver .

A custom JsonWriter increments a counter when an object is started and then decrements it again when it ends.

public class CustomJsonTextWriter : JsonTextWriter
{
    public CustomJsonTextWriter(TextWriter textWriter) : base(textWriter) {}

    public int CurrentDepth { get; private set; }

    public override void WriteStartObject()
    {
        CurrentDepth++;
        base.WriteStartObject();
    }

    public override void WriteEndObject()
    {
        CurrentDepth--;
        base.WriteEndObject();
    }
}

A custom ContractResolver applies a special ShouldSerialize predicate on all properties that will be used to verify the current depth.

public class CustomContractResolver : DefaultContractResolver
{
    private readonly Func<bool> _includeProperty;

    public CustomContractResolver(Func<bool> includeProperty)
    {
        _includeProperty = includeProperty;
    }

    protected override JsonProperty CreateProperty(
        MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        var shouldSerialize = property.ShouldSerialize;
        property.ShouldSerialize = obj => _includeProperty() &&
                                          (shouldSerialize == null ||
                                           shouldSerialize(obj));
        return property;
    }
}

The following method shows how these two custom classes work together.

public static string SerializeObject(object obj, int maxDepth)
{
    using (var strWriter = new StringWriter())
    {
        using (var jsonWriter = new CustomJsonTextWriter(strWriter))
        {
            Func<bool> include = () => jsonWriter.CurrentDepth <= maxDepth;
            var resolver = new CustomContractResolver(include);
            var serializer = new JsonSerializer {ContractResolver = resolver};
            serializer.Serialize(jsonWriter, obj);
        }
        return strWriter.ToString();
    }
}

The following test code demonstrates limiting the maximum depth to 1 and 2 levels respectively.

var obj = new Node {
    Name = "one",
    Child = new Node {
        Name = "two",
        Child = new Node {
            Name = "three"
        }
    }
};
var txt1 = SerializeObject(obj, 1);
var txt2 = SerializeObject(obj, 2);

public class Node
{
    public string Name { get; set; }
    public Node Child { get; set; }
}

You could use reflection to inspect the object and make a copy that changes each property value as needed. Coincidentally I've just made public a new library that makes this kind of thing really easy. You can get it here: https://github.com/jamietre/IQObjectMapper

Here's an example of the code you would use

var newInstance = ObjectMapper.Map(obj,(value,del) => {
    return value !=null && value.GetType().IsClass ?
        null :
        value;
    });

The "Map" method iterates through each property of the object, and calls a Func<object,IDelegateInfo> for each (IDelegateInfo having reflection info such as the property name, type, etc.). The function returns the new value for each property. So in this example, I just test the value of each property to see if it's a class, and if so, return null; if not, return the original value.

Another more expressive way to do it:

var obj = new MyObject();

// map the object to a new dictionary        

var dict = ObjectMapper.ToDictionary(obj);

// iterate through each item in the dictionary, a key/value pair
// representing each property 

foreach (KeyValuePair<string,object> kvp in dict) {
    if (kvp.Value!=null && kvp.Value.GetType().IsClass) {
        dict[kvp.Key]=null;
    }
}

// map back to an instance

var newObject = ObjectMapper.ToNew<MyObject>(dict);

In either case, the value of newInstance.myChildren (and any other properties that are non-value-typed) will be null. You could easily change the rules for what happens in this mapping.

Hope this helps. Btw - from your comment it sounds like JSON isn't really your goal but just something you thought would help you achieve it. If you want to end up with json, just serialize the output of this, eg

string json = JavaScriptSerializer.Serialize(newObject);

But I wouldn't involve json if that is was just a means to an end; if you want to stay in CLR objects then no real need to use JSON as an intermediary.

First, I wanted to say all credit should go to Nathan Baulch. This is an adaptation of his answer combined with using the MaxDepth in settings. Thanks for your help Nathan!

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;

namespace Helpers
{
    public class JsonNetResult : JsonResult
    {
        public JsonNetResult()
        {
            Settings = new JsonSerializerSettings
            {
                ReferenceLoopHandling = ReferenceLoopHandling.Error
            };
        }

        public JsonSerializerSettings Settings { get; private set; }

        public override void ExecuteResult(ControllerContext context)
        {
            if (context == null)
                throw new ArgumentNullException("context");
            if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
                throw new InvalidOperationException("JSON GET is not allowed");

            HttpResponseBase response = context.HttpContext.Response;
            response.ContentType = string.IsNullOrEmpty(this.ContentType) ? "application/json" : this.ContentType;

            if (this.ContentEncoding != null)
                response.ContentEncoding = this.ContentEncoding;
            if (this.Data == null)
                return;

            var scriptSerializer = JsonSerializer.Create(this.Settings);

            using (var sw = new StringWriter())
            {
                if (Settings.MaxDepth != null)
                {
                    using (var jsonWriter = new JsonNetTextWriter(sw))
                    {
                        Func<bool> include = () => jsonWriter.CurrentDepth <= Settings.MaxDepth;
                        var resolver = new JsonNetContractResolver(include);
                        this.Settings.ContractResolver = resolver;
                        var serializer = JsonSerializer.Create(this.Settings);
                        serializer.Serialize(jsonWriter, Data);
                    }
                    response.Write(sw.ToString());
                }
                else
                {
                    scriptSerializer.Serialize(sw, this.Data);
                    response.Write(sw.ToString());
                }
            }
        }
    }

    public class JsonNetTextWriter : JsonTextWriter
    {
        public JsonNetTextWriter(TextWriter textWriter) : base(textWriter) { }

        public int CurrentDepth { get; private set; }

        public override void WriteStartObject()
        {
            CurrentDepth++;
            base.WriteStartObject();
        }

        public override void WriteEndObject()
        {
            CurrentDepth--;
            base.WriteEndObject();
        }
    }

    public class JsonNetContractResolver : DefaultContractResolver
    {
        private readonly Func<bool> _includeProperty;

        public JsonNetContractResolver(Func<bool> includeProperty)
        {
            _includeProperty = includeProperty;
        }

        protected override JsonProperty CreateProperty(
            MemberInfo member, MemberSerialization memberSerialization)
        {
            var property = base.CreateProperty(member, memberSerialization);
            var shouldSerialize = property.ShouldSerialize;
            property.ShouldSerialize = obj => _includeProperty() &&
                                              (shouldSerialize == null ||
                                               shouldSerialize(obj));
            return property;
        }
    }
}

Use:

// instantiating JsonNetResult to handle circular reference issue.
var result = new JsonNetResult
{
    Data = <<The results to be returned>>,
    JsonRequestBehavior = JsonRequestBehavior.AllowGet,
    Settings =
        {
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
            MaxDepth = 1
        }
};

return result;

If you want to use this in a ASP.NET Core project, maybe you cant implement your own JsonTextWriter. But you can custom the DefaultContractResolver and IValueProvider

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;

namespace customserialization
{
    /// <summary>
    /// IValueProvider personalizado  para manejar max depth level
    /// </summary>
    public class CustomDynamicValueProvider : DynamicValueProvider, IValueProvider
    {
        MemberInfo _memberInfo;
        MaxDepthHandler _levelHandler;

        public CustomDynamicValueProvider(MemberInfo memberInfo, MaxDepthHandler levelHandler) : base(memberInfo)
        {
            _memberInfo = memberInfo;
            _levelHandler = levelHandler;
        }

        public new object GetValue(object target)
        {
            //Si el valor a serializar es un objeto se incrementa el nivel de profundidad. En el caso de las listas el nivel se incrementa en el evento OnSerializing
            if (((PropertyInfo)_memberInfo).PropertyType.IsClass) this._levelHandler.IncrementLevel();

            var rv = base.GetValue(target);

            //Al finalizar la obtención del valor se decrementa. En el caso de las listas el nivel se decrementa en el evento OnSerialized
            if (((PropertyInfo)_memberInfo).PropertyType.IsClass) this._levelHandler.DecrementLevel();

            return rv;
        }
    }

    /// <summary>
    /// Maneja los niveles de serialización
    /// </summary>
    public class MaxDepthHandler
    {
        int _maxDepth;
        int _currentDepthLevel;

        /// <summary>
        /// Nivel actual
        /// </summary>
        public int CurrentDepthLevel { get { return _currentDepthLevel; } }

        public MaxDepthHandler(int maxDepth)
        {
            this._currentDepthLevel = 1;
            this._maxDepth = maxDepth;
        }

        /// <summary>
        /// Incrementa el nivel actual
        /// </summary>
        public void IncrementLevel()
        {
            this._currentDepthLevel++;
        }

        /// <summary>
        /// Decrementa el nivel actual
        /// </summary>
        public void DecrementLevel()
        {
            this._currentDepthLevel--;
        }

        /// <summary>
        /// Determina si se alcanzó el nivel actual
        /// </summary>
        /// <returns></returns>
        public bool IsMaxDepthLevel()
        {
            return !(this._currentDepthLevel < this._maxDepth);
        }
    }

    public class ShouldSerializeContractResolver : DefaultContractResolver
    {

        MaxDepthHandler _levelHandler;

        public ShouldSerializeContractResolver(int maxDepth)
        {
            this._levelHandler = new MaxDepthHandler(maxDepth);
        }


        void OnSerializing(object o, System.Runtime.Serialization.StreamingContext context)
        {
            //Antes de serializar una lista se incrementa el nivel. En el caso de los objetos el nivel se incrementa en el método GetValue del IValueProvider
            if (o.GetType().IsGenericList())
                _levelHandler.IncrementLevel();
        }

        void OnSerialized(object o, System.Runtime.Serialization.StreamingContext context)
        {
            //Despues de serializar una lista se decrementa el nivel. En el caso de los objetos el nivel se decrementa en el método GetValue del IValueProvider
            if (o.GetType().IsGenericList())
                _levelHandler.DecrementLevel();
        }

        protected override JsonContract CreateContract(Type objectType)
        {
            var contract = base.CreateContract(objectType);
            contract.OnSerializingCallbacks.Add(new SerializationCallback(OnSerializing));
            contract.OnSerializedCallbacks.Add(new SerializationCallback(OnSerialized));

            return contract;
        }


        protected override IValueProvider CreateMemberValueProvider(MemberInfo member)
        {
            var rv = base.CreateMemberValueProvider(member);

            if (rv is DynamicValueProvider) //DynamicValueProvider es el valueProvider usado en general
            {
                //Utilizo mi propio ValueProvider, que utilizar el levelHandler
                rv = new CustomDynamicValueProvider(member, this._levelHandler);
            }

            return rv;
        }

        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
        {
            JsonProperty property = base.CreateProperty(member, memberSerialization);

            var isObjectOrList = ((PropertyInfo)member).PropertyType.IsGenericList() || ((PropertyInfo)member).PropertyType.IsClass;



            property.ShouldSerialize =
                    instance =>
                    {
                        var shouldSerialize = true;
                        //Si se alcanzo el nivel maximo y la propiedad (member) actual a serializar es un objeto o lista no se serializa (shouldSerialize = false)
                        if (_levelHandler.IsMaxDepthLevel() && isObjectOrList)
                            shouldSerialize = false;                        

                        return shouldSerialize;
                    };

            return property;
        }



    }

    public static class Util
    {
        public static bool IsGenericList(this Type type)
        {
            foreach (Type @interface in type.GetInterfaces())
            {
                if (@interface.IsGenericType)
                {
                    if (@interface.GetGenericTypeDefinition() == typeof(ICollection<>))
                    {
                        // if needed, you can also return the type used as generic argument
                        return true;
                    }
                }
            }
            return false;
        }
    }
}

and use this in your controller

        [HttpGet]
        public IActionResult TestJSON()
        {
            var obj = new Thing
            {
                id = 1,
                reference = new Thing
                {
                    id = 2,
                    reference = new Thing
                    {
                        id = 3,
                        reference = new Thing
                        {
                            id = 4
                        }
                    }
                }
            };
            var settings = new JsonSerializerSettings()
            {
                ContractResolver = new ShouldSerializeContractResolver(2),
            };

            return new JsonResult(obj, settings);

        }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM