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

问题描述

编辑:我昨天在.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
      ],"SCREEN_BOUNDS": [
        485,159,64,64
      ]
    }
  }
}

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

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

解决方法

System.Text.Json当前无法实现。让我们考虑一下可能性:

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

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

  2. Utf8JsonWriter无法修改或控制缩进,因为Options是仅获得struct值的属性。

  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,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,JsonSerializerOptions options)
       at System.Text.Json.JsonSerializer.WriteCoreString(Object value,JsonSerializerOptions options)
       at System.Text.Json.JsonSerializer.Serialize[TValue](TValue value,JsonSerializerOptions options)
    

    演示小提琴#1 here

  4. 在.Net Core 3.1中,如果我创建一个自定义JsonConverter<T>并创建一个预先格式化的JsonDocument,然后将其写出来,则文档将在编写时重新格式化。 / p>

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

    class CollectionFormattingConverter<TCollection,JsonSerializerOptions options)
        {
            var copy = options.Clone();
            copy.WriteIndented = false;
            using var doc = JsonExtensions.JsonDocumentFromObject(new CollectionSurrogate<TCollection,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,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
          ],"SCREEN_BOUNDS": [
            485,159,64,64
          ]
        }
      }
    }
    

    演示小提琴#2 here

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

    演示小提琴#3 here

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

您可能想打开一个issue来请求此功能,因为关于如何使用Json.NET(可以在转换器中完成)存在多个流行的问题:

,

面临同样的问题。为了 json 的简单性,我需要在一行中编写数组。

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

解决方案:

  • 我使用反射创建具有所需选项的 Utf8JsonWriter 克隆(请参阅类 Utf8JsonWriterCopier.cs)
  • 要检查 API 是否未更改 Clone 调用 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,ReadCommentHandling = options.ReadCommentHandling,};

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

        return serializerOptions;
    }
}

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...