如何仅对某些属性应用缩进序列化?

How to apply indenting serialization only to some properties?

我想以人类可读的方式将 .NET 对象序列化为 JSON,但我想更好地控制对象的属性或数组的元素是否最终在它们自己的一行上。

目前我正在使用 JSON.NET 的 JsonConvert.SerializeObject(object, Formatting, JsonSerializerSettings) 方法进行序列化,但似乎我只能应用 Formatting.Indented(所有元素在单独的行上)或 Formatting.None(所有内容都在一行中,没有任何空格)整个对象的全局格式化规则。有没有一种方法可以在默认情况下全局使用缩进,但对于某些 类 或属性将其关闭,例如使用属性或其他参数?

为了帮助您理解问题,这里有一些输出示例。使用 Formatting.None:

{"array":["element 1","element 2","element 3"],"object":{"property1":"value1","property2":"value2"}}

使用Formatting.Indented

{
  "array": [
    "element 1",
    "element 2",
    "element 3"
  ],
  "object": {
    "property1": "value1",
    "property2":"value2"
  }
}

我想看的:

{
  "array": ["element 1","element 2","element 3"],
  "object": {"property1":"value1","property2":"value2"}
}

(我意识到我的问题可能与 this one 略有相关,但那里的评论完全没有抓住重点,实际上并没有提供有效的答案。)

一种可能是为您需要特殊处理的特定类型编写自定义 Json 转换器并为它们切换格式:

class Program
{
    static void Main()
    {
        var root = new Root
        {
            Array = new[] { "element 1", "element 2", "element 3" },
            Object = new Obj
            {
                Property1 = "value1",
                Property2 = "value2",
            },
        };
        var settings = new JsonSerializerSettings
        {
            Formatting = Formatting.Indented,
        };
        settings.Converters.Add(new MyConverter());

        string json = JsonConvert.SerializeObject(root, settings);
        Console.WriteLine(json);
    }
}

public class Root
{
    public string[] Array { get; set; }
    public Obj Object { get; set; }
}

public class Obj
{
    public string Property1 { get; set; }
    public string Property2 { get; set; }
}

class MyConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(string[]) || objectType == typeof(Obj);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteRawValue(JsonConvert.SerializeObject(value, Formatting.None));
    }
}

这将输出:

{
  "Array": ["element 1","element 2","element 3"],
  "Object": {"Property1":"value1","Property2":"value2"}
}

我还为此使用了一个转换器(根据 Darin Dimitrov 的回答),但我没有调用 WriteRawValue(),而是为每个元素使用序列化程序;这确保将使用适用于元素类型的任何自定义转换器。

但是请注意,此转换器仅在少数基本类型的数组上运行,它没有使用 Newtonsoft.Json 逻辑来确定什么应该序列化为数组以及什么是基本类型,主要是因为该代码是内部代码,我想避免维护它的副本。

总的来说,我觉得转换器不适合用于格式化任务,但我认为它们是当前 API 中的唯一选择。理想情况下,API 会提供更多格式设置选项,或者可能更好地支持转换器中的自定义格式设置 API。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace JsonProto
{
    /// <summary>
    /// A JsonConverter that modifies formatting of arrays, such that the array elements are serialised to a single line instead of one element per line
    /// preceded by indentation whitespace.
    /// This converter handles writing JSON only; CanRead returns false.
    /// </summary>
    /// <remarks>
    /// This converter/formatter applies to arrays only and not other collection types. Ideally we would use the existing logic within Newtonsoft.Json for
    /// identifying collections of items, as this handles a number of special cases (e.g. string implements IEnumerable over the string characters). In order
    /// to avoid duplicating in lots of logic, instead this converter handles only Arrays of a handful of selected primitive types.
    /// </remarks>
    public class ArrayNoFormattingConverter : JsonConverter
    {
        # region Static Fields    

        static HashSet<Type> _primitiveTypeSet = 
            new HashSet<Type> 
            { 
                typeof(char),
                typeof(char?),
                typeof(bool),
                typeof(bool?),
                typeof(sbyte),
                typeof(sbyte?),
                typeof(short),
                typeof(short?),
                typeof(ushort),
                typeof(ushort?),
                typeof(int),
                typeof(int?),
                typeof(byte),
                typeof(byte?),
                typeof(uint),
                typeof(uint?),
                typeof(long),
                typeof(long?),
                typeof(ulong),
                typeof(ulong?),
                typeof(float),
                typeof(float?),
                typeof(double),
                typeof(double?),
                typeof(decimal),
                typeof(decimal?),
                typeof(string),
                typeof(DateTime),
                typeof(DateTime?),
            };

        #endregion

        #region Properties

        /// <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)
        {
            // Note. Ideally this would match the test for JsonContractType.Array in DefaultContractResolver.CreateContract(),
            // but that code is all internal to Newtonsoft.Json.
            // Here we elect to take over conversion for Arrays only.
            if(!objectType.IsArray) {
                return false;
            }

            // Fast/efficient way of testing for multiple possible primitive types.
            Type elemType = objectType.GetElementType();
            return _primitiveTypeSet.Contains(elemType);
        }

        /// <summary>
        /// Gets a value indicating whether this <see cref="JsonConverter"/> can read JSON.
        /// </summary>
        /// <value>Always returns <c>false</c>.</value>
        public override bool CanRead
        {
            get { return false; }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Reads the JSON representation of the object. (Not implemented on this converter).
        /// </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>
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }

        /// <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>
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            Formatting formatting = writer.Formatting;
            writer.WriteStartArray();
            try
            {
                writer.Formatting = Formatting.None;
                foreach(object childValue in ((System.Collections.IEnumerable)value)) {
                    serializer.Serialize(writer, childValue);
                }
            }
            finally
            {
                writer.WriteEndArray();
                writer.Formatting = formatting;
            }
        }

        #endregion
    }
}