在 System.Text.Json 中是否可以指定自定义缩进规则?

In System.Text.Json is it possible to specify custom indentation rules?

编辑:我昨天在 .Net runtime repo 上提出了一个问题,该问题已被“layomia”关闭并显示以下消息:“添加这样的扩展点会带来较低的性能成本-level reader 和 writer,在 perf 和 functionality/benefit 之间没有很好的平衡。提供这样的配置不在 System.Text.Json 路线图上。“

当设置 JsonSerializerOptions.WriteIndented = true 时缩进看起来像这样 json...

{
  "TILESET": "tilesets/HOW_TO_GET_TILESET_NAME_?",
  "TILES": {
    "TILE_1": {
      "NAME": "auto_tile_18",
      "TEXTURE_BOUNDS": [
        304,
        16,
        16,
        16
      ],
      "SCREEN_BOUNDS": [
        485,
        159,
        64,
        64
      ]
    }
  }
}

有没有办法将自动缩进更改为这样的...

{
  "TILESET": "tilesets/HOW_TO_GET_TILESET_NAME_?",
  "TILES": 
  {
    "TILE_1": 
    {
      "NAME": "auto_tile_18",
      "TEXTURE_BOUNDS": [304, 16, 16,16],
      "SCREEN_BOUNDS": [485, 159, 64, 64]
    }
  }
}

目前 System.Text.Json(从 .NET 5 开始)无法做到这一点。让我们考虑一下可能性:

  1. JsonSerializerOptions has no method to control indentation other than the Boolean property WriteIndented:

    Gets or sets a value that defines whether JSON should use pretty printing.

  2. Utf8JsonWriter has no method to modify or control indentation, as Options 是 get-only struct 值 属性.

  3. 在 .Net Core 3.1 中,如果我为您的 TEXTURE_BOUNDSSCREEN_BOUNDS 列表创建一个 custom JsonConverter<T> 并尝试在序列化期间设置 options.WriteIndented = false;, a System.InvalidOperationException:序列化程序选项一旦发生序列化或反序列化就无法更改 将抛出异常。

    具体来说,如果我创建以下转换器:

    class CollectionFormattingConverter<TCollection, TItem> : JsonConverter<TCollection> where TCollection : class, ICollection<TItem>, new()
    {
        public override TCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            => JsonSerializer.Deserialize<CollectionSurrogate<TCollection, TItem>>(ref reader, options)?.BaseCollection;
    
        public override void Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
        {
            var old = options.WriteIndented;
            try
            {
                options.WriteIndented = false;
                JsonSerializer.Serialize(writer, new CollectionSurrogate<TCollection, TItem>(value), options);
            }
            finally
            {
                options.WriteIndented = old;
            }
        }
    }
    
    public class CollectionSurrogate<TCollection, TItem> : ICollection<TItem> where TCollection : ICollection<TItem>, new()
    {
        public TCollection BaseCollection { get; }
    
        public CollectionSurrogate() { this.BaseCollection = new TCollection(); }
        public CollectionSurrogate(TCollection baseCollection) { this.BaseCollection = baseCollection ?? throw new ArgumentNullException(); }
    
        public void Add(TItem item) => BaseCollection.Add(item);
        public void Clear() => BaseCollection.Clear();
        public bool Contains(TItem item) => BaseCollection.Contains(item);
        public void CopyTo(TItem[] array, int arrayIndex) => BaseCollection.CopyTo(array, arrayIndex);
        public int Count => BaseCollection.Count;
        public bool IsReadOnly => BaseCollection.IsReadOnly;
        public bool Remove(TItem item) => BaseCollection.Remove(item);
        public IEnumerator<TItem> GetEnumerator() => BaseCollection.GetEnumerator();
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => ((IEnumerable)BaseCollection).GetEnumerator();
    }
    

    以及以下数据模型:

    public partial class Root
    {
        [JsonPropertyName("TILESET")]
        public string Tileset { get; set; }
        [JsonPropertyName("TILES")]
        public Tiles Tiles { get; set; }
    }
    
    public partial class Tiles
    {
        [JsonPropertyName("TILE_1")]
        public Tile1 Tile1 { get; set; }
    }
    
    public partial class Tile1
    {
        [JsonPropertyName("NAME")]
        public string Name { get; set; }
    
        [JsonPropertyName("TEXTURE_BOUNDS")]
        [JsonConverter(typeof(CollectionFormattingConverter<List<long>, long>))]
        public List<long> TextureBounds { get; set; }
    
        [JsonPropertyName("SCREEN_BOUNDS")]
        [JsonConverter(typeof(CollectionFormattingConverter<List<long>, long>))]
        public List<long> ScreenBounds { get; set; }
    }
    

    然后序列化Root抛出以下异常:

    Failed with unhandled exception: 
    System.InvalidOperationException: Serializer options cannot be changed once serialization or deserialization has occurred.
       at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_SerializerOptionsImmutable()
       at System.Text.Json.JsonSerializerOptions.set_WriteIndented(Boolean value)
       at CollectionFormattingConverter`2.Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
       at System.Text.Json.JsonPropertyInfoNotNullable`4.OnWrite(WriteStackFrame& current, Utf8JsonWriter writer)
       at System.Text.Json.JsonPropertyInfo.Write(WriteStack& state, Utf8JsonWriter writer)
       at System.Text.Json.JsonSerializer.Write(Utf8JsonWriter writer, Int32 originalWriterDepth, Int32 flushThreshold, JsonSerializerOptions options, WriteStack& state)
       at System.Text.Json.JsonSerializer.WriteCore(Utf8JsonWriter writer, Object value, Type type, JsonSerializerOptions options)
       at System.Text.Json.JsonSerializer.WriteCore(PooledByteBufferWriter output, Object value, Type type, JsonSerializerOptions options)
       at System.Text.Json.JsonSerializer.WriteCoreString(Object value, Type type, JsonSerializerOptions options)
       at System.Text.Json.JsonSerializer.Serialize[TValue](TValue value, JsonSerializerOptions options)
    

    演示 fiddle #1 here.

  4. 在 .Net Core 3.1 中,如果我创建一个自定义 JsonConverter<T> 创建一个 pre-formatted JsonDocument 然后将其写出,文档将被重新格式化正如所写。

    即如果我创建以下转换器:

    class CollectionFormattingConverter<TCollection, TItem> : JsonConverter<TCollection> where TCollection : class, ICollection<TItem>, new()
    {
        public override TCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            => JsonSerializer.Deserialize<CollectionSurrogate<TCollection, TItem>>(ref reader, options)?.BaseCollection;
    
        public override void Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
        {
            var copy = options.Clone();
            copy.WriteIndented = false;
            using var doc = JsonExtensions.JsonDocumentFromObject(new CollectionSurrogate<TCollection, TItem>(value), copy);
            Debug.WriteLine("Preformatted JsonDocument: {0}", doc.RootElement);
            doc.WriteTo(writer);
        }
    }
    
    public static partial class JsonExtensions
    {
        public static JsonSerializerOptions Clone(this JsonSerializerOptions options)
        {
            if (options == null)
                return new JsonSerializerOptions();
            //In .Net 5 a copy constructor will be introduced for JsonSerializerOptions.  Use the following in that version.
            //return new JsonSerializerOptions(options);
            //In the meantime copy manually.
            var clone = new JsonSerializerOptions
            {
                AllowTrailingCommas = options.AllowTrailingCommas,
                DefaultBufferSize = options.DefaultBufferSize,
                DictionaryKeyPolicy = options.DictionaryKeyPolicy,
                Encoder = options.Encoder,
                IgnoreNullValues = options.IgnoreNullValues,
                IgnoreReadOnlyProperties = options.IgnoreReadOnlyProperties,
                MaxDepth = options.MaxDepth,
                PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive,
                PropertyNamingPolicy = options.PropertyNamingPolicy,
                ReadCommentHandling= options.ReadCommentHandling,
                WriteIndented = options.WriteIndented,
            };
            foreach (var converter in options.Converters)
                clone.Converters.Add(converter);
            return clone;
        }
    
        // Copied from this answer 
        // To 
        // By https://whosebug.com/users/3744182/dbc
    
        public static JsonDocument JsonDocumentFromObject<TValue>(TValue value, JsonSerializerOptions options = default) 
            => JsonDocumentFromObject(value, typeof(TValue), options);
    
        public static JsonDocument JsonDocumentFromObject(object value, Type type, JsonSerializerOptions options = default)
        {
            var bytes = JsonSerializer.SerializeToUtf8Bytes(value, options);
            return JsonDocument.Parse(bytes);
        }
    }
    

    生成完全缩进的 JSON,尽管中间 JsonDocument doc 序列化时没有缩进:

    {
      "TILESET": "tilesets/HOW_TO_GET_TILESET_NAME_?",
      "TILES": {
        "TILE_1": {
          "NAME": "auto_tile_18",
          "TEXTURE_BOUNDS": [
            304,
            16,
            16,
            16
          ],
          "SCREEN_BOUNDS": [
            485,
            159,
            64,
            64
          ]
        }
      }
    }
    

    演示 fiddle #2 here.

  5. 最后,在 .Net Core 3.1 中,如果我创建一个自定义 JsonConverter<T> 来克隆传入的 JsonSerializerOptions,在副本上修改 WriteIndented,然后使用复制的设置递归序列化 - WriteIndented 的修改值被忽略。

    演示 fiddle #3 here.

    显然 JsonConverter 体系结构将在 .Net 5 中得到广泛增强,因此您可以 re-test 在它发布时使用此选项。

您可能想打开一个 issue 请求此功能,因为有多个关于如何使用 Json.NET 执行此操作的常见问题(可以使用转换器完成):

面临同样的问题。为了json简单起见,我需要将数组写成一行。

最新版本在这里:https://github.com/micro-elements/MicroElements.Metadata/blob/master/src/MicroElements.Metadata.SystemTextJson/SystemTextJson/Utf8JsonWriterCopier.cs

解决方案:

  • 我使用反射创建具有所需选项的 Utf8JsonWriter 克隆(参见 class Utf8JsonWriterCopier.cs)
  • 检查 API 是否未更改克隆调用 Utf8JsonWriterCopier.AssertReflectionStateIsValid,您也可以在测试中使用它

用法:

  • 创建 Utf8JsonWriter 的 NotIndented 副本
  • 写数组
  • 将内部状态复制回原始作者

样本:

if (Options.WriteArraysInOneRow && propertyType.IsArray && writer.Options.Indented)
{
    // Creates NotIndented writer
    Utf8JsonWriter writerCopy = writer.CloneNotIndented();

    // PropertyValue
    JsonSerializer.Serialize(writerCopy, propertyValue.ValueUntyped, propertyType, options);

    // Needs to copy internal state back to writer
    writerCopy.CopyStateTo(writer);
}

Utf8JsonWriterCopier.cs

/// <summary>
/// Helps to copy <see cref="Utf8JsonWriter"/> with other <see cref="JsonWriterOptions"/>.
/// This is not possible with public API so Reflection is used to copy writer internals.
/// See also: 
/// Usage:
/// <code>
/// if (Options.WriteArraysInOneRow and propertyType.IsArray and writer.Options.Indented)
/// {
///     // Create NotIndented writer
///     Utf8JsonWriter writerCopy = writer.CloneNotIndented();
///
///     // Write array
///     JsonSerializer.Serialize(writerCopy, array, options);
///
///     // Copy internal state back to writer
///     writerCopy.CopyStateTo(writer);
/// }
/// </code>
/// </summary>
public static class Utf8JsonWriterCopier
{
    private class Utf8JsonWriterReflection
    {
        private IReadOnlyCollection<string> FieldsToCopyNames { get; } = new[] { "_arrayBufferWriter", "_memory", "_inObject", "_tokenType", "_bitStack", "_currentDepth" };

        private IReadOnlyCollection<string> PropertiesToCopyNames { get; } = new[] { "BytesPending", "BytesCommitted" };

        private FieldInfo[] Fields { get; }

        private PropertyInfo[] Properties { get; }

        internal FieldInfo OutputField { get; }

        internal FieldInfo StreamField { get; }

        internal FieldInfo[] FieldsToCopy { get; }

        internal PropertyInfo[] PropertiesToCopy { get; }

        public Utf8JsonWriterReflection()
        {
            Fields = typeof(Utf8JsonWriter).GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
            Properties = typeof(Utf8JsonWriter).GetProperties(BindingFlags.Instance | BindingFlags.Public);
            OutputField = Fields.FirstOrDefault(info => info.Name == "_output")!;
            StreamField = Fields.FirstOrDefault(info => info.Name == "_stream")!;

            FieldsToCopy = FieldsToCopyNames
                .Select(name => Fields.FirstOrDefault(info => info.Name == name))
                .Where(info => info != null)
                .ToArray();

            PropertiesToCopy = PropertiesToCopyNames
                .Select(name => Properties.FirstOrDefault(info => info.Name == name))
                .Where(info => info != null)
                .ToArray();
        }

        public void AssertStateIsValid()
        {
            if (OutputField == null)
                throw new ArgumentException("Field _output is not found. API Changed!");
            if (StreamField == null)
                throw new ArgumentException("Field _stream is not found. API Changed!");
            if (FieldsToCopy.Length != FieldsToCopyNames.Count)
                throw new ArgumentException("Not all FieldsToCopy found in Utf8JsonWriter. API Changed!");
            if (PropertiesToCopy.Length != PropertiesToCopyNames.Count)
                throw new ArgumentException("Not all FieldsToCopy found in Utf8JsonWriter. API Changed!");
        }
    }

    private static readonly Utf8JsonWriterReflection _reflectionCache = new Utf8JsonWriterReflection();

    /// <summary>
    /// Checks that reflection API is valid.
    /// </summary>
    public static void AssertReflectionStateIsValid()
    {
        _reflectionCache.AssertStateIsValid();
    }

    /// <summary>
    /// Clones <see cref="Utf8JsonWriter"/> with new <see cref="JsonWriterOptions"/>.
    /// </summary>
    /// <param name="writer">Source writer.</param>
    /// <param name="newOptions">Options to use in new writer.</param>
    /// <returns>New copy of <see cref="Utf8JsonWriter"/> with new options.</returns>
    public static Utf8JsonWriter Clone(this Utf8JsonWriter writer, JsonWriterOptions newOptions)
    {
        AssertReflectionStateIsValid();

        Utf8JsonWriter writerCopy;

        // Get internal output to use in new writer
        IBufferWriter<byte>? output = (IBufferWriter<byte>?)_reflectionCache.OutputField.GetValue(writer);
        if (output != null)
        {
            // Create copy
            writerCopy = new Utf8JsonWriter(output, newOptions);
        }
        else
        {
            // Get internal stream to use in new writer
            Stream? stream = (Stream?)_reflectionCache.StreamField.GetValue(writer);

            // Create copy
            writerCopy = new Utf8JsonWriter(stream, newOptions);
        }

        // Copy internal state
        writer.CopyStateTo(writerCopy);

        return writerCopy;
    }

    /// <summary>
    /// Clones <see cref="Utf8JsonWriter"/> and sets <see cref="JsonWriterOptions.Indented"/> to false.
    /// </summary>
    /// <param name="writer">Source writer.</param>
    /// <returns>New copy of <see cref="Utf8JsonWriter"/>.</returns>
    public static Utf8JsonWriter CloneNotIndented(this Utf8JsonWriter writer)
    {
        JsonWriterOptions newOptions = writer.Options;
        newOptions.Indented = false;

        return Clone(writer, newOptions);
    }

    /// <summary>
    /// Clones <see cref="Utf8JsonWriter"/> and sets <see cref="JsonWriterOptions.Indented"/> to true.
    /// </summary>
    /// <param name="writer">Source writer.</param>
    /// <returns>New copy of <see cref="Utf8JsonWriter"/>.</returns>
    public static Utf8JsonWriter CloneIndented(this Utf8JsonWriter writer)
    {
        JsonWriterOptions newOptions = writer.Options;
        newOptions.Indented = true;

        return Clone(writer, newOptions);
    }

    /// <summary>
    /// Copies internal state of one writer to another.
    /// </summary>
    /// <param name="sourceWriter">Source writer.</param>
    /// <param name="targetWriter">Target writer.</param>
    public static void CopyStateTo(this Utf8JsonWriter sourceWriter, Utf8JsonWriter targetWriter)
    {
        foreach (var fieldInfo in _reflectionCache.FieldsToCopy)
        {
            fieldInfo.SetValue(targetWriter, fieldInfo.GetValue(sourceWriter));
        }

        foreach (var propertyInfo in _reflectionCache.PropertiesToCopy)
        {
            propertyInfo.SetValue(targetWriter, propertyInfo.GetValue(sourceWriter));
        }
    }

    /// <summary>
    /// Clones <see cref="JsonSerializerOptions"/>.
    /// </summary>
    /// <param name="options">Source options.</param>
    /// <returns>New instance of <see cref="JsonSerializerOptions"/>.</returns>
    public static JsonSerializerOptions Clone(this JsonSerializerOptions options)
    {
        JsonSerializerOptions serializerOptions = new JsonSerializerOptions()
        {
            AllowTrailingCommas = options.AllowTrailingCommas,
            WriteIndented = options.WriteIndented,
            PropertyNamingPolicy = options.PropertyNamingPolicy,
            DefaultBufferSize = options.DefaultBufferSize,
            DictionaryKeyPolicy = options.DictionaryKeyPolicy,
            Encoder = options.Encoder,
            IgnoreNullValues = options.IgnoreNullValues,
            IgnoreReadOnlyProperties = options.IgnoreReadOnlyProperties,
            MaxDepth = options.MaxDepth,
            PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive,
            ReadCommentHandling = options.ReadCommentHandling,
        };

        foreach (JsonConverter jsonConverter in options.Converters)
        {
            serializerOptions.Converters.Add(jsonConverter);
        }

        return serializerOptions;
    }
}