【问题标题】:In System.Text.Json is it possible to specify custom indentation rules?在 System.Text.Json 中是否可以指定自定义缩进规则?
【发布时间】:2020-08-12 12:48:25
【问题描述】:

编辑:我昨天在.Net runtime repo 提出了一个问题,该问题已被“layomia”关闭,并带有以下消息:“添加这样的扩展点会降低低级读写器的性能成本,并且在性能和功能/利益之间没有很好的平衡。提供这样的配置不在 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]
    }
  }
}

【问题讨论】:

    标签: c# indentation system.text.json


    【解决方案1】:

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

    1. JsonSerializerOptions 除了布尔属性WriteIndented 之外,没有其他方法可以控制缩进:

      获取或设置一个定义 JSON 是否应该使用漂亮打印的值。

    2. Utf8JsonWriter 无法修改或控制缩进,因为Options 是一个只能获取的struct-valued 属性。

    3. 在.Net Core 3.1 中,如果我为您的TEXTURE_BOUNDSSCREEN_BOUNDS 列表创建custom JsonConverter<T> 并在序列化过程中尝试设置options.WriteIndented = false;,则会出现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)
      

      演示小提琴 #1 here.

    4. 在 .Net Core 3.1 中,如果我创建一个自定义 JsonConverter&lt;T&gt; 并创建一个预格式化的 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 https://stackoverflow.com/a/62998253/3744182
          // To https://stackoverflow.com/questions/62996999/convert-object-to-system-text-json-jsonelement
          // By https://stackoverflow.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);
          }
      }
      

      尽管中间 JsonDocument doc 是在没有缩进的情况下序列化的,但仍会生成完全缩进的 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
            ]
          }
        }
      }
      

      演示小提琴#2 here.

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

      演示小提琴#3 here.

      显然,JsonConverter 架构将在 .Net 5 中得到广泛增强,因此您可以在发布时重新测试此选项。

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

    【讨论】:

    • 感谢您的全面回答。我在这里打开了一个问题:github.com/dotnet/runtime/issues/40731
    • 我有一个解决方案。对它还有兴趣吗?
    • @Alexey.Petriashev - 如果需要,您可以添加另一个答案。
    【解决方案2】:

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

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

    解决方案:

    • 我使用反射创建具有所需选项的 Utf8JsonWriter 的克隆(请参阅类 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: https://stackoverflow.com/questions/63376873/in-system-text-json-is-it-possible-to-specify-custom-indentation-rules.
    /// 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;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2010-11-05
      • 2017-11-09
      • 1970-01-01
      • 2019-10-02
      • 1970-01-01
      • 1970-01-01
      • 2017-05-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多