问题描述
编辑:我昨天在.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
当前无法实现。让我们考虑一下可能性:
-
JsonSerializerOptions
除了布尔属性WriteIndented
之外没有其他方法可以控制缩进:获取或设置一个值,该值定义JSON是否应使用漂亮的打印。
-
Utf8JsonWriter
无法修改或控制缩进,因为Options
是仅获得struct
值的属性。 -
在.Net Core 3.1中,如果我为您的
TEXTURE_BOUNDS
和SCREEN_BOUNDS
列表创建了customJsonConverter<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。
-
在.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。
-
最后,在.Net Core 3.1中,如果我创建一个自定义
JsonConverter<T>
来克隆传入的JsonSerializerOptions
,在副本上修改WriteIndented
,然后使用递归序列化复制的设置-WriteIndented
的修改值将被忽略。演示小提琴#3 here。
显然,
JsonConverter
体系结构将在.Net 5中得到广泛增强,因此您可以在发布此选项时重新对其进行测试。
您可能想打开一个issue来请求此功能,因为关于如何使用Json.NET(可以在转换器中完成)存在多个流行的问题:
- How to apply indenting serialization only to some properties?
- Newtonsoft inline formatting for subelement while serializing
- Creating JSON without array indentation
面临同样的问题。为了 json 的简单性,我需要在一行中编写数组。
解决方案:
- 我使用反射创建具有所需选项的 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;
}
}