Skip to content

Commit

Permalink
feat: Add System.Text.Json serialization/deserialization supports (#1…
Browse files Browse the repository at this point in the history
…0217)

* chore: refactoring JsonUtility code to support newtonsoftjson/systemtextjson switching based on type

* chore: add json/yaml serialization test infrastructure

* chore: switch XRefMap serialization logics to use System.Text.Json

* chore: add json roundtrip test with JsonUtility

* chore: Add MarkdigExtensionSetting json roundtrip tests

* chore: add ManagedReference YAML roundtrip tests

* chore: add UniversalReference YAML roundtrip tests

* chore: add ApiPage YAML roundtrip tests

* chore: add FilterConfig YAML roundtrip tests

* chore: Add Manifest model json roundtrip tests

* chore: change JavascriptEncoder settings and add related tests

* chore: add TOC model YAML roundtrip tests

* chore: add `dummy property name` for deserialize `metadata` key data

* chore: add FileMapping/FileItems converters and JSON roundtrip tests

* chore: disable `MarkdigExtensionSettingConverter` JSON formatting

* chore: add ListWithStringFallback converters and JSON roundtrip test

* chore: add FileMetadataPairs converters and JSON roundtrip test

* chore: add MergeJsonConfig converters and JSON roundtrip test

* chore: add BuildJsonConfig JSON roundtrip test

* chore: add MetadataJsonConfig JSON roundtrip test

* chore: add DocfxConfig JSON roundtrip test

* chore: Switch to use SystemTextJson by default and fix related tests

* chore: modify converter to throw JsonException if unexpected token found

* chore: merge changes of FileMetadataPairsConverter.cs
  • Loading branch information
filzrev authored Oct 19, 2024
1 parent 512fdf5 commit 2f8cf18
Show file tree
Hide file tree
Showing 127 changed files with 14,628 additions and 405 deletions.
7 changes: 4 additions & 3 deletions src/Docfx.App/Config/FileMetadataPairs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ namespace Docfx;
/// </summary>
/// <see cref="BuildJsonConfig.FileMetadata"/>
/// <see cref="MergeJsonItemConfig.FileMetadata"/>
[Newtonsoft.Json.JsonConverter(typeof(FileMetadataPairsConverter))]
[Newtonsoft.Json.JsonConverter(typeof(FileMetadataPairsConverter.NewtonsoftJsonConverter))]
[System.Text.Json.Serialization.JsonConverter(typeof(FileMetadataPairsConverter.SystemTextJsonConverter))]
internal class FileMetadataPairs
{
// Order matters, the latter one overrides the former one
Expand All @@ -28,9 +29,9 @@ public IReadOnlyList<FileMetadataPairsItem> Items
/// <summary>
/// Initializes a new instance of the <see cref="FileMetadataPairs"/> class.
/// </summary>
public FileMetadataPairs(List<FileMetadataPairsItem> items)
public FileMetadataPairs(IEnumerable<FileMetadataPairsItem> items)
{
_items = items;
_items = items.ToList();
}

/// <summary>
Expand Down
63 changes: 63 additions & 0 deletions src/Docfx.App/Config/FileMetadataPairsConverter.NewtonsoftJson.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Docfx.Common;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Docfx;

internal partial class FileMetadataPairsConverter
{
/// <summary>
/// JsonConverter for FileMetadataPairs
/// </summary>
internal class NewtonsoftJsonConverter : JsonConverter
{
/// <inheritdoc/>
public override bool CanConvert(Type objectType)
{
return objectType == typeof(FileMetadataPairs);
}

/// <inheritdoc/>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
IEnumerable<JToken> jItems;
if (reader.TokenType == JsonToken.StartObject)
{
jItems = JContainer.Load(reader);
}
else throw new JsonReaderException($"{reader.TokenType} is not a valid {objectType.Name}.");
return new FileMetadataPairs(jItems.Select(ParseItem).ToList());
}

/// <inheritdoc/>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteStartObject();
foreach (var item in ((FileMetadataPairs)value).Items)
{
writer.WritePropertyName(item.Glob.Raw);
writer.WriteRawValue(JsonUtility.Serialize(item.Value));
}
writer.WriteEndObject();
}

private static FileMetadataPairsItem ParseItem(JToken item)
{
if (item.Type == JTokenType.Property)
{
JProperty jProperty = item as JProperty;
var pattern = jProperty.Name;
var rawValue = jProperty.Value;
return new FileMetadataPairsItem(pattern, rawValue);
}
else
{
throw new JsonReaderException($"Unsupported value {item} (type: {item.Type}).");
}
}
}
}
78 changes: 78 additions & 0 deletions src/Docfx.App/Config/FileMetadataPairsConverter.SystemTextJson.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Docfx.Common;

#nullable enable

namespace Docfx;

internal partial class FileMetadataPairsConverter
{
/// <summary>
/// JsonConverter for FileMetadataPairs
/// </summary>
internal class SystemTextJsonConverter : JsonConverter<FileMetadataPairs>
{
public override FileMetadataPairs Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException($"{reader.TokenType} is not a valid {typeToConvert.Name}.");
}

using var document = JsonDocument.ParseValue(ref reader);
var properties = document.RootElement.EnumerateObject();
var items = properties.Select(x => new FileMetadataPairsItem(x.Name, ToInferredType(x.Value))).ToArray();
return new FileMetadataPairs(items);
}

public override void Write(Utf8JsonWriter writer, FileMetadataPairs value, JsonSerializerOptions options)
{
writer.WriteStartObject();
foreach (var item in value.Items)
{
writer.WritePropertyName(item.Glob.Raw);
writer.WriteRawValue(JsonUtility.Serialize(item.Value));
}
writer.WriteEndObject();
}

/// <summary>
/// Convert JsonElement to .NET object.
/// </summary>
private static object? ToInferredType(JsonElement elem)
{
switch (elem.ValueKind)
{
case JsonValueKind.Null:
return null;
case JsonValueKind.True:
return true;
case JsonValueKind.False:
return false;
case JsonValueKind.String when elem.TryGetDateTime(out DateTime datetime):
return datetime;
case JsonValueKind.String:
return elem.GetString();
case JsonValueKind.Array:
return elem.EnumerateArray().Select(ToInferredType).ToArray();
case JsonValueKind.Object:
var properties = elem.EnumerateObject();
return properties.ToDictionary(x => x.Name, x => ToInferredType(x.Value));
case JsonValueKind.Number when elem.TryGetInt32(out int intValue):
return intValue;
case JsonValueKind.Number when elem.TryGetInt64(out long longValue):
return longValue;
case JsonValueKind.Number:
return elem.GetDouble();
case JsonValueKind.Undefined:
default:
throw new JsonException($"JsonValueKind({elem.ValueKind}) is not supported.");
}
}
}
}
46 changes: 1 addition & 45 deletions src/Docfx.App/Config/FileMetadataPairsConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,50 +11,6 @@ namespace Docfx;
/// <summary>
/// JsonConverter for FileMetadataPairs
/// </summary>
internal class FileMetadataPairsConverter : JsonConverter
internal partial class FileMetadataPairsConverter
{
/// <inheritdoc/>
public override bool CanConvert(Type objectType)
{
return objectType == typeof(FileMetadataPairs);
}

/// <inheritdoc/>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
IEnumerable<JToken> jItems;
if (reader.TokenType == JsonToken.StartObject)
{
jItems = JContainer.Load(reader);
}
else throw new JsonReaderException($"{reader.TokenType} is not a valid {objectType.Name}.");
return new FileMetadataPairs(jItems.Select(ParseItem).ToList());
}

/// <inheritdoc/>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteStartObject();
foreach (var item in ((FileMetadataPairs)value).Items)
{
writer.WritePropertyName(item.Glob.Raw);
writer.WriteRawValue(JsonUtility.Serialize(item.Value));
}
writer.WriteEndObject();
}

private static FileMetadataPairsItem ParseItem(JToken item)
{
if (item.Type == JTokenType.Property)
{
JProperty jProperty = item as JProperty;
var pattern = jProperty.Name;
var rawValue = jProperty.Value;
return new FileMetadataPairsItem(pattern, rawValue);
}
else
{
throw new JsonReaderException($"Unsupported value {item} (type: {item.Type}).");
}
}
}
16 changes: 16 additions & 0 deletions src/Docfx.App/Config/FileMetadataPairsItem.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;
using System.Text.Json.Serialization;
using Docfx.Common;
using Docfx.Glob;

Expand All @@ -20,6 +22,7 @@ internal class FileMetadataPairsItem
/// <summary>
/// JObject, no need to transform it to object as the metadata value will not be used but only to be serialized
/// </summary>
[System.Text.Json.Serialization.JsonConverter(typeof(JsonElementConverter))]
public object Value { get; }

/// <summary>
Expand All @@ -31,3 +34,16 @@ public FileMetadataPairsItem(string pattern, object value)
Value = ConvertToObjectHelper.ConvertJObjectToObject(value);
}
}

internal class JsonElementConverter : JsonConverter<JsonElement>
{
public override JsonElement Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}

public override void Write(Utf8JsonWriter writer, JsonElement value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
3 changes: 2 additions & 1 deletion src/Docfx.App/Config/ListWithStringFallback.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ namespace Docfx;
/// <summary>
/// ListWithStringFallback.
/// </summary>
[Newtonsoft.Json.JsonConverter(typeof(ListWithStringFallbackConverter))]
[Newtonsoft.Json.JsonConverter(typeof(ListWithStringFallbackConverter.NewtonsoftJsonConverter))]
[System.Text.Json.Serialization.JsonConverter(typeof(ListWithStringFallbackConverter.SystemTextJsonConverter))]
internal class ListWithStringFallback : List<string>
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Docfx;


internal partial class ListWithStringFallbackConverter
{
/// <summary>
/// JsonConverter for <see cref="ListWithStringFallback"/>.
/// </summary>
internal class NewtonsoftJsonConverter : JsonConverter
{
/// <inheritdoc/>
public override bool CanConvert(Type objectType)
{
return objectType == typeof(FileMapping);
}

/// <inheritdoc/>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var model = new ListWithStringFallback();
var value = reader.Value;
IEnumerable<JToken> jItems;
if (reader.TokenType == JsonToken.StartArray)
{
jItems = JArray.Load(reader);
}
else if (reader.TokenType == JsonToken.StartObject)
{
jItems = JContainer.Load(reader);
}
else if (reader.TokenType == JsonToken.String)
{
jItems = JRaw.Load(reader);
}
else
{
jItems = JObject.Load(reader);
}

if (jItems is JValue)
{
model.Add(jItems.ToString());
}
else
{
foreach (var item in jItems)
{
model.Add(item.ToString());
}
}

return model;
}

/// <inheritdoc/>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteStartArray();
foreach (var item in (ListWithStringFallback)value)
{
serializer.Serialize(writer, item);
}
writer.WriteEndArray();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using YamlDotNet.Serialization;

namespace Docfx;


internal partial class ListWithStringFallbackConverter
{
/// <summary>
/// JsonConverter for <see cref="ListWithStringFallback"/>.
/// </summary>
internal class SystemTextJsonConverter : JsonConverter<ListWithStringFallback>
{
public override ListWithStringFallback Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var tokenType = reader.TokenType;
switch (tokenType)
{
case JsonTokenType.String:
{
var value = reader.GetString();
return new ListWithStringFallback([value]);
}
case JsonTokenType.StartArray:
{
var items = JsonSerializer.Deserialize<string[]>(ref reader, options);
return new ListWithStringFallback(items);
}
case JsonTokenType.StartObject:
{
using var document = JsonDocument.ParseValue(ref reader);
JsonElement root = document.RootElement;
var values = root.EnumerateObject().Select(x=>x.ToString());
return new ListWithStringFallback(values);
}
default:
throw new JsonException($"TokenType({reader.TokenType}) is not supported.");
}
}

public override void Write(Utf8JsonWriter writer, ListWithStringFallback value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var item in value)
{
writer.WriteStringValue(item);
}
writer.WriteEndArray();
}
}
}
Loading

0 comments on commit 2f8cf18

Please sign in to comment.