Как сериализовать или десериализовать объект JSON на определенную глубину в С#?

Мне нужен только первый уровень глубины объекта (я не хочу никаких детей). Я готов использовать любую доступную библиотеку. Большинство библиотек просто выдают исключение, когда достигается глубина рекурсии, а не просто игнорируется. Если это невозможно, существует ли способ игнорировать сериализацию определенных членов с определенным типом данных?

Изменить: Скажем, у меня есть такой объект:

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

Я хочу удалить любые дочерние объекты (сложные типы даже), чтобы вернуть такой объект:

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

Ответ 1

Это возможно в Json.NET, используя некоторую координацию между JsonWriter и сериализатором ContractResolver.

Пользовательский JsonWriter увеличивает счетчик при запуске объекта и затем уменьшает его, когда он заканчивается.

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();
    }
}

Пользовательский ContractResolver применяет специальный ShouldSerialize предикат для всех свойств, которые будут использоваться для проверки текущей глубины.

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;
    }
}

Следующий метод показывает, как эти два пользовательских класса работают вместе.

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();
    }
}

Следующий тестовый код демонстрирует ограничение максимальной глубины на уровни 1 и 2 соответственно.

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; }
}

Ответ 2

Вы можете использовать отражение для проверки объекта и создания копии, которая при необходимости изменяет каждое значение свойства. Кстати, я только что обнародовал новую библиотеку, которая делает эту вещь очень простой. Вы можете получить его здесь: https://github.com/jamietre/IQObjectMapper

Вот пример кода, который вы использовали бы

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

Метод "Карта" выполняет итерацию через каждое свойство объекта и вызывает Func<object,IDelegateInfo> для каждого (IDelegateInfo с информацией отражения, такой как имя свойства, тип и т.д.). Функция возвращает новое значение для каждого свойства. Поэтому в этом примере я просто проверяю значение каждого свойства, чтобы увидеть, является ли он классом, и если да, то возвратите null; если нет, верните исходное значение.

Еще один выразительный способ сделать это:

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);

В любом случае значение newInstance.myChildren (и любые другие свойства, которые не являются значениями) будет нулевым. Вы можете легко изменить правила того, что происходит в этом сопоставлении.

Надеюсь, это поможет. Btw - из вашего комментария это звучит, как JSON, это не ваша цель, а просто то, что, по вашему мнению, поможет вам в этом. Если вы хотите закончить с помощью json, просто выполните сериализацию вывода этого файла, например

string json = JavaScriptSerializer.Serialize(newObject);

Но я бы не привлек json, если это было всего лишь средством к концу; если вы хотите остаться в объектах CLR, тогда не нужно использовать JSON в качестве посредника.

Ответ 3

Во-первых, я хотел сказать, что все кредиты должны отправиться в Натан Баулч. Это адаптация его ответа в сочетании с использованием MaxDepth в настройках. Спасибо за вашу помощь Натан!

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;
        }
    }
}

Использование:

// 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;

Ответ 4

Если вы хотите использовать это в проекте ASP.NET Core, возможно, вы не можете реализовать свой собственный JsonTextWriter. Но вы можете настраивать DefaultContractResolver и 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;
        }
    }
}

и использовать его в контроллере

        [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);

        }