From b1dbc4d0c5b8fdc04a27a0d7d161b11c2522e4ce Mon Sep 17 00:00:00 2001 From: sunil-lakshman <104969541+sunil-lakshman@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:02:08 +0530 Subject: [PATCH] Migrated to newtonsoft.json to System.Text.Json and .NET 10.x support --- CHANGELOG.md | 8 + .../Contentstack.Utils.Tests.csproj | 3 +- Contentstack.Utils.Tests/DefaultRenderTest.cs | 7 +- .../Helpers/NodeParser.cs | 28 +-- .../Mocks/CustomRenderOptionMock.cs | 5 +- Contentstack.Utils.Tests/Mocks/GQLModel.cs | 26 ++- .../VariantAliasesTest.cs | 166 +++++++++--------- Contentstack.Utils/Contentstack.Utils.csproj | 3 +- .../Converters/NodeJsonConverter.cs | 90 ++++++++-- .../Converters/RTEJsonConverter.cs | 163 ++++++++++++++--- Contentstack.Utils/Interfaces/IEdges.cs | 4 +- Contentstack.Utils/JsonAttrValue.cs | 65 +++++++ Contentstack.Utils/Models/JsonRTENode.cs | 13 +- Contentstack.Utils/Models/JsonRTENodes.cs | 11 +- Contentstack.Utils/Models/Metadata.cs | 13 +- Contentstack.Utils/Models/Node.cs | 7 +- Contentstack.Utils/Models/Options.cs | 21 ++- Contentstack.Utils/Utils.cs | 83 +++++---- Directory.Build.props | 2 +- 19 files changed, 489 insertions(+), 229 deletions(-) create mode 100644 Contentstack.Utils/JsonAttrValue.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 72dbee7..b7be9a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### Version: 2.0.0-beta.1 +#### Date: April-27-2026 +- **Breaking:** Replaced **Newtonsoft.Json** with **System.Text.Json** across the package. The `Newtonsoft.Json` package reference is removed; add `System.Text.Json` (or rely on the BCL on supported runtimes) as needed in consuming projects. +- **Breaking:** Variant metadata APIs that previously took `JObject` / `JArray` now use `System.Text.Json.Nodes.JsonObject` and `JsonArray` (`GetVariantAliases`, `GetVariantMetadataTags`, and obsolete `GetDataCsvariantsAttribute` overloads). +- JSON serialization uses the same model attributes with `System.Text.Json.Serialization` (`JsonPropertyName`, `JsonConverter`), including custom converters for RTE/GQL-shaped JSON and **path-mapped** embedded models (`PathMappedJsonConverter`). +- RTE JSON deserialization tolerates **trailing commas** when using the documented test/helper patterns (`AllowTrailingCommas`); attribute dictionaries may surface **`JsonElement`** values instead of boxed strings—use helpers or unwrap explicitly if you access `Node.attrs` directly. +- Internal: `LangVersion` set to **latest** for multi-target builds; utilities normalize attribute values where the HTML pipeline expects strings. + ### Version: 1.2.0 #### Date: March-31-2026 - Added `GetVariantMetadataTags(JObject, string)` and `GetVariantMetadataTags(JArray, string)` as the canonical API for building the `data-csvariants` payload (same behavior as the previous helpers). diff --git a/Contentstack.Utils.Tests/Contentstack.Utils.Tests.csproj b/Contentstack.Utils.Tests/Contentstack.Utils.Tests.csproj index 4d4e3d4..f8e4299 100644 --- a/Contentstack.Utils.Tests/Contentstack.Utils.Tests.csproj +++ b/Contentstack.Utils.Tests/Contentstack.Utils.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net10.0 false $(Version) @@ -9,7 +9,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Contentstack.Utils.Tests/DefaultRenderTest.cs b/Contentstack.Utils.Tests/DefaultRenderTest.cs index 6f44ee3..96d1155 100644 --- a/Contentstack.Utils.Tests/DefaultRenderTest.cs +++ b/Contentstack.Utils.Tests/DefaultRenderTest.cs @@ -5,6 +5,7 @@ using Contentstack.Utils.Tests.Helpers; using Contentstack.Utils.Tests.Constants; using System; +using Contentstack.Utils; namespace Contentstack.Utils.Tests { @@ -178,9 +179,9 @@ public void testLinkhDocument() string result = defaultRender.RenderNode("a", nodeLink, (nodes) => { return text; }); - string url = nodeLink.attrs.ContainsKey("url") ? (string)nodeLink.attrs["url"] : ""; - string target = nodeLink.attrs.ContainsKey("target") ? (string)nodeLink.attrs["target"] : ""; - string title = nodeLink.attrs.ContainsKey("title") ? (string)nodeLink.attrs["title"] : ""; + string url = nodeLink.attrs.ContainsKey("url") ? JsonAttrValue.AsString(nodeLink.attrs["url"]) : ""; + string target = nodeLink.attrs.ContainsKey("target") ? JsonAttrValue.AsString(nodeLink.attrs["target"]) : ""; + string title = nodeLink.attrs.ContainsKey("title") ? JsonAttrValue.AsString(nodeLink.attrs["title"]) : ""; Assert.Equal($"Text To set Link", result); } diff --git a/Contentstack.Utils.Tests/Helpers/NodeParser.cs b/Contentstack.Utils.Tests/Helpers/NodeParser.cs index 4ee6ac7..6648754 100644 --- a/Contentstack.Utils.Tests/Helpers/NodeParser.cs +++ b/Contentstack.Utils.Tests/Helpers/NodeParser.cs @@ -1,31 +1,35 @@ -using Contentstack.Utils.Interfaces; +using System.Text.Json; +using Contentstack.Utils.Interfaces; using Contentstack.Utils.Models; using Contentstack.Utils.Tests.Constants; using Contentstack.Utils.Tests.Mocks; -using Newtonsoft.Json; namespace Contentstack.Utils.Tests.Helpers { public class NodeParser { - public static Node parse(string jsonNode) + private static readonly JsonSerializerOptions SerializerSettings = new JsonSerializerOptions { - JsonSerializerSettings SerializerSettings = new JsonSerializerSettings(); - JsonSerializer Serializer = JsonSerializer.Create(SerializerSettings); + AllowTrailingCommas = true, + }; - return JsonConvert.DeserializeObject(jsonNode, SerializerSettings); + public static Node parse(string jsonNode) + { + return JsonSerializer.Deserialize(jsonNode, SerializerSettings); } } + public class GQLParser { - public static GQLModel parse(string jsonNode, string embedConnection = null) where T: IEmbeddedObject + private static readonly JsonSerializerOptions SerializerSettings = new JsonSerializerOptions + { + AllowTrailingCommas = true, + }; + + public static GQLModel parse(string jsonNode, string embedConnection = null) where T : IEmbeddedObject { var data = JsonToHtmlConstants.KGQLModel(jsonNode, embedConnection); - JsonSerializerSettings SerializerSettings = new JsonSerializerSettings(); - JsonSerializer Serializer = JsonSerializer.Create(SerializerSettings); - return JsonConvert.DeserializeObject>(data, SerializerSettings); + return JsonSerializer.Deserialize>(data, SerializerSettings); } } - } - diff --git a/Contentstack.Utils.Tests/Mocks/CustomRenderOptionMock.cs b/Contentstack.Utils.Tests/Mocks/CustomRenderOptionMock.cs index 53b37da..1983150 100644 --- a/Contentstack.Utils.Tests/Mocks/CustomRenderOptionMock.cs +++ b/Contentstack.Utils.Tests/Mocks/CustomRenderOptionMock.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Contentstack.Utils; using Contentstack.Utils.Interfaces; using Contentstack.Utils.Models; using HtmlAgilityPack; @@ -17,9 +18,9 @@ public override string RenderNode(string nodeType, Node node, NodeChildrenCallBa case "a": if (node.attrs.ContainsKey("target")) { - return $"{callBack(node.children)}"; + return $"{callBack(node.children)}"; } - return $"{callBack(node.children)}"; + return $"{callBack(node.children)}"; } return base.RenderNode(nodeType, node, callBack); } diff --git a/Contentstack.Utils.Tests/Mocks/GQLModel.cs b/Contentstack.Utils.Tests/Mocks/GQLModel.cs index 586ff39..d4cc33d 100644 --- a/Contentstack.Utils.Tests/Mocks/GQLModel.cs +++ b/Contentstack.Utils.Tests/Mocks/GQLModel.cs @@ -2,33 +2,32 @@ using Contentstack.Utils.Converters; using Contentstack.Utils.Interfaces; using Contentstack.Utils.Models; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Contentstack.Utils.Tests.Mocks { - public class GQLModel where T: IEmbeddedObject + public class GQLModel where T : IEmbeddedObject { - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] public JsonRTENodes multiplerte { get; set; } public JsonRTENode singlerte { get; set; } } - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] + [JsonConverter(typeof(PathMappedJsonConverter))] public class EntryModel : IEmbeddedEntry { - [JsonProperty("system.uid")] + [JsonPropertyName("system.uid")] public string Uid { get; set; } - [JsonProperty("system.content_type_uid")] + [JsonPropertyName("system.content_type_uid")] public string ContentTypeUid { get; set; } - [JsonProperty("title")] + [JsonPropertyName("title")] public string Title { get; @@ -36,34 +35,34 @@ public string Title } } - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] + [JsonConverter(typeof(PathMappedJsonConverter))] public class AssetModel : IEmbeddedAsset { - [JsonProperty("system.uid")] + [JsonPropertyName("system.uid")] public string Uid { get; set; } - [JsonProperty("system.content_type_uid")] + [JsonPropertyName("system.content_type_uid")] public string ContentTypeUid { get; set; } - [JsonProperty("title")] + [JsonPropertyName("title")] public string Title { get; set; } - [JsonProperty("filename")] + [JsonPropertyName("filename")] public string FileName { get; set; } - [JsonProperty("url")] + [JsonPropertyName("url")] public string Url { get; @@ -71,4 +70,3 @@ public string Url } } } - diff --git a/Contentstack.Utils.Tests/VariantAliasesTest.cs b/Contentstack.Utils.Tests/VariantAliasesTest.cs index 868f60e..1260b03 100644 --- a/Contentstack.Utils.Tests/VariantAliasesTest.cs +++ b/Contentstack.Utils.Tests/VariantAliasesTest.cs @@ -1,20 +1,20 @@ using System; using System.Collections.Generic; using System.IO; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; using Xunit; namespace Contentstack.Utils.Tests { public class VariantAliasesTest { - private static JObject ReadJsonRoot(string fileName) + private static JsonObject ReadJsonRoot(string fileName) { var path = Path.Combine(AppContext.BaseDirectory, "Resources", fileName); - return JObject.Parse(File.ReadAllText(path)); + return JsonNode.Parse(File.ReadAllText(path)).AsObject(); } - private static HashSet JsonArrayToStringSet(JArray arr) + private static HashSet JsonArrayToStringSet(JsonArray arr) { var set = new HashSet(); foreach (var t in arr) @@ -27,15 +27,15 @@ private static HashSet JsonArrayToStringSet(JArray arr) [Fact] public void GetVariantAliases_SingleEntry_ReturnsAliases() { - JObject full = ReadJsonRoot("variantsSingleEntry.json"); - JObject entry = (JObject)full["entry"]; + JsonObject full = ReadJsonRoot("variantsSingleEntry.json"); + JsonObject entry = full["entry"].AsObject(); const string contentTypeUid = "movie"; - JObject result = Utils.GetVariantAliases(entry, contentTypeUid); + JsonObject result = Utils.GetVariantAliases(entry, contentTypeUid); Assert.True(result["entry_uid"] != null && !string.IsNullOrEmpty(result["entry_uid"].ToString())); Assert.Equal(contentTypeUid, result["contenttype_uid"].ToString()); - JArray variants = (JArray)result["variants"]; + JsonArray variants = result["variants"].AsArray(); Assert.NotNull(variants); var aliasSet = JsonArrayToStringSet(variants); Assert.Equal( @@ -46,20 +46,20 @@ public void GetVariantAliases_SingleEntry_ReturnsAliases() [Fact] public void GetVariantMetadataTags_SingleEntry_ReturnsJsonArrayString() { - JObject full = ReadJsonRoot("variantsSingleEntry.json"); - JObject entry = (JObject)full["entry"]; + JsonObject full = ReadJsonRoot("variantsSingleEntry.json"); + JsonObject entry = full["entry"].AsObject(); const string contentTypeUid = "movie"; - JObject result = Utils.GetVariantMetadataTags(entry, contentTypeUid); + JsonObject result = Utils.GetVariantMetadataTags(entry, contentTypeUid); Assert.True(result["data-csvariants"] != null); string dataCsvariantsStr = result["data-csvariants"].ToString(); - JArray arr = JArray.Parse(dataCsvariantsStr); + JsonArray arr = JsonNode.Parse(dataCsvariantsStr).AsArray(); Assert.Single(arr); - JObject first = (JObject)arr[0]; + JsonObject first = arr[0].AsObject(); Assert.True(first["entry_uid"] != null && !string.IsNullOrEmpty(first["entry_uid"].ToString())); Assert.Equal(contentTypeUid, first["contenttype_uid"].ToString()); - var aliasSet = JsonArrayToStringSet((JArray)first["variants"]); + var aliasSet = JsonArrayToStringSet(first["variants"].AsArray()); Assert.Equal( new HashSet { "cs_personalize_0_0", "cs_personalize_0_3" }, aliasSet); @@ -68,80 +68,80 @@ public void GetVariantMetadataTags_SingleEntry_ReturnsJsonArrayString() [Fact] public void GetVariantAliases_MultipleEntries_ReturnsOneResultPerEntryWithUid() { - JObject full = ReadJsonRoot("variantsEntries.json"); - JArray entries = (JArray)full["entries"]; + JsonObject full = ReadJsonRoot("variantsEntries.json"); + JsonArray entries = full["entries"].AsArray(); const string contentTypeUid = "movie"; - JArray result = Utils.GetVariantAliases(entries, contentTypeUid); + JsonArray result = Utils.GetVariantAliases(entries, contentTypeUid); Assert.NotNull(result); Assert.Equal(3, result.Count); - JObject first = (JObject)result[0]; + JsonObject first = result[0].AsObject(); Assert.True(first["entry_uid"] != null && !string.IsNullOrEmpty(first["entry_uid"].ToString())); Assert.Equal(contentTypeUid, first["contenttype_uid"].ToString()); - var firstSet = JsonArrayToStringSet((JArray)first["variants"]); + var firstSet = JsonArrayToStringSet(first["variants"].AsArray()); Assert.Equal( new HashSet { "cs_personalize_0_0", "cs_personalize_0_3" }, firstSet); - JObject second = (JObject)result[1]; + JsonObject second = result[1].AsObject(); Assert.True(second["entry_uid"] != null && !string.IsNullOrEmpty(second["entry_uid"].ToString())); - Assert.Single((JArray)second["variants"]); - Assert.Equal("cs_personalize_0_0", ((JArray)second["variants"])[0].ToString()); + Assert.Single(second["variants"].AsArray()); + Assert.Equal("cs_personalize_0_0", second["variants"].AsArray()[0].ToString()); - JObject third = (JObject)result[2]; + JsonObject third = result[2].AsObject(); Assert.True(third["entry_uid"] != null && !string.IsNullOrEmpty(third["entry_uid"].ToString())); - Assert.Empty((JArray)third["variants"]); + Assert.Empty(third["variants"].AsArray()); } [Fact] public void GetVariantMetadataTags_MultipleEntries_ReturnsJsonArrayString() { - JObject full = ReadJsonRoot("variantsEntries.json"); - JArray entries = (JArray)full["entries"]; + JsonObject full = ReadJsonRoot("variantsEntries.json"); + JsonArray entries = full["entries"].AsArray(); const string contentTypeUid = "movie"; - JObject result = Utils.GetVariantMetadataTags(entries, contentTypeUid); + JsonObject result = Utils.GetVariantMetadataTags(entries, contentTypeUid); Assert.True(result["data-csvariants"] != null); string dataCsvariantsStr = result["data-csvariants"].ToString(); - JArray arr = JArray.Parse(dataCsvariantsStr); + JsonArray arr = JsonNode.Parse(dataCsvariantsStr).AsArray(); Assert.Equal(3, arr.Count); - Assert.True(((JObject)arr[0])["entry_uid"] != null && !string.IsNullOrEmpty(((JObject)arr[0])["entry_uid"].ToString())); - Assert.Equal(2, ((JArray)((JObject)arr[0])["variants"]).Count); - Assert.True(((JObject)arr[1])["entry_uid"] != null && !string.IsNullOrEmpty(((JObject)arr[1])["entry_uid"].ToString())); - Assert.Single((JArray)((JObject)arr[1])["variants"]); - Assert.True(((JObject)arr[2])["entry_uid"] != null && !string.IsNullOrEmpty(((JObject)arr[2])["entry_uid"].ToString())); - Assert.Empty((JArray)((JObject)arr[2])["variants"]); + Assert.True(arr[0].AsObject()["entry_uid"] != null && !string.IsNullOrEmpty(arr[0].AsObject()["entry_uid"].ToString())); + Assert.Equal(2, arr[0].AsObject()["variants"].AsArray().Count); + Assert.True(arr[1].AsObject()["entry_uid"] != null && !string.IsNullOrEmpty(arr[1].AsObject()["entry_uid"].ToString())); + Assert.Single(arr[1].AsObject()["variants"].AsArray()); + Assert.True(arr[2].AsObject()["entry_uid"] != null && !string.IsNullOrEmpty(arr[2].AsObject()["entry_uid"].ToString())); + Assert.Empty(arr[2].AsObject()["variants"].AsArray()); } [Fact] public void GetVariantAliases_ThrowsWhenEntryNull() { - Assert.Throws(() => Utils.GetVariantAliases((JObject)null, "landing_page")); + Assert.Throws(() => Utils.GetVariantAliases((JsonObject)null, "landing_page")); } [Fact] public void GetVariantAliases_ThrowsWhenContentTypeUidNull() { - JObject full = ReadJsonRoot("variantsSingleEntry.json"); - JObject entry = (JObject)full["entry"]; + JsonObject full = ReadJsonRoot("variantsSingleEntry.json"); + JsonObject entry = full["entry"].AsObject(); Assert.Throws(() => Utils.GetVariantAliases(entry, null)); } [Fact] public void GetVariantAliases_ThrowsWhenContentTypeUidEmpty() { - JObject full = ReadJsonRoot("variantsSingleEntry.json"); - JObject entry = (JObject)full["entry"]; + JsonObject full = ReadJsonRoot("variantsSingleEntry.json"); + JsonObject entry = full["entry"].AsObject(); Assert.Throws(() => Utils.GetVariantAliases(entry, "")); } [Fact] public void GetVariantMetadataTags_WhenEntryNull_ReturnsEmptyArrayString() { - JObject result = Utils.GetVariantMetadataTags((JObject)null, "landing_page"); + JsonObject result = Utils.GetVariantMetadataTags((JsonObject)null, "landing_page"); Assert.True(result["data-csvariants"] != null); Assert.Equal("[]", result["data-csvariants"].ToString()); } @@ -149,49 +149,49 @@ public void GetVariantMetadataTags_WhenEntryNull_ReturnsEmptyArrayString() [Fact] public void GetVariantAliases_ThrowsWhenUidMissing() { - var entry = new JObject { ["title"] = "no-uid" }; + var entry = new JsonObject { ["title"] = "no-uid" }; Assert.Throws(() => Utils.GetVariantAliases(entry, "movie")); } [Fact] public void GetVariantAliases_ThrowsWhenUidNull() { - var entry = new JObject { ["uid"] = JValue.CreateNull() }; + var entry = new JsonObject { ["uid"] = JsonNode.Parse("null") }; Assert.Throws(() => Utils.GetVariantAliases(entry, "movie")); } [Fact] public void GetVariantAliases_Batch_ThrowsWhenContentTypeUidNull() { - var entries = new JArray { new JObject { ["uid"] = "a" } }; + var entries = new JsonArray { new JsonObject { ["uid"] = "a" } }; Assert.Throws(() => Utils.GetVariantAliases(entries, null)); } [Fact] public void GetVariantAliases_Batch_ThrowsWhenContentTypeUidEmpty() { - var entries = new JArray { new JObject { ["uid"] = "a" } }; + var entries = new JsonArray { new JsonObject { ["uid"] = "a" } }; Assert.Throws(() => Utils.GetVariantAliases(entries, "")); } [Fact] public void GetVariantMetadataTags_WhenEntriesArrayNull_ReturnsEmptyArrayString() { - JObject result = Utils.GetVariantMetadataTags((JArray)null, "movie"); + JsonObject result = Utils.GetVariantMetadataTags((JsonArray)null, "movie"); Assert.Equal("[]", result["data-csvariants"].ToString()); } [Fact] public void GetVariantMetadataTags_Batch_ThrowsWhenContentTypeUidNull() { - var entries = new JArray { new JObject { ["uid"] = "a" } }; + var entries = new JsonArray { new JsonObject { ["uid"] = "a" } }; Assert.Throws(() => Utils.GetVariantMetadataTags(entries, null)); } [Fact] public void GetVariantMetadataTags_Batch_ThrowsWhenContentTypeUidEmpty() { - var entries = new JArray { new JObject { ["uid"] = "a" } }; + var entries = new JsonArray { new JsonObject { ["uid"] = "a" } }; Assert.Throws(() => Utils.GetVariantMetadataTags(entries, "")); } @@ -199,85 +199,85 @@ public void GetVariantMetadataTags_Batch_ThrowsWhenContentTypeUidEmpty() public void GetDataCsvariantsAttribute_DelegatesToGetVariantMetadataTags() { #pragma warning disable CS0618 // Type or member is obsolete — intentional coverage of backward-compatible alias - JObject full = ReadJsonRoot("variantsSingleEntry.json"); - JObject entry = (JObject)full["entry"]; + JsonObject full = ReadJsonRoot("variantsSingleEntry.json"); + JsonObject entry = full["entry"].AsObject(); const string contentTypeUid = "movie"; - JObject canonical = Utils.GetVariantMetadataTags(entry, contentTypeUid); - JObject legacy = Utils.GetDataCsvariantsAttribute(entry, contentTypeUid); - Assert.True(JToken.DeepEquals(canonical, legacy)); + JsonObject canonical = Utils.GetVariantMetadataTags(entry, contentTypeUid); + JsonObject legacy = Utils.GetDataCsvariantsAttribute(entry, contentTypeUid); + Assert.True(JsonNode.DeepEquals(canonical, legacy)); - JObject fullMulti = ReadJsonRoot("variantsEntries.json"); - JArray entries = (JArray)fullMulti["entries"]; - JObject canonicalBatch = Utils.GetVariantMetadataTags(entries, contentTypeUid); - JObject legacyBatch = Utils.GetDataCsvariantsAttribute(entries, contentTypeUid); - Assert.True(JToken.DeepEquals(canonicalBatch, legacyBatch)); + JsonObject fullMulti = ReadJsonRoot("variantsEntries.json"); + JsonArray entries = fullMulti["entries"].AsArray(); + JsonObject canonicalBatch = Utils.GetVariantMetadataTags(entries, contentTypeUid); + JsonObject legacyBatch = Utils.GetDataCsvariantsAttribute(entries, contentTypeUid); + Assert.True(JsonNode.DeepEquals(canonicalBatch, legacyBatch)); - JObject nullEntryLegacy = Utils.GetDataCsvariantsAttribute((JObject)null, "x"); - JObject nullEntryCanonical = Utils.GetVariantMetadataTags((JObject)null, "x"); - Assert.True(JToken.DeepEquals(nullEntryCanonical, nullEntryLegacy)); + JsonObject nullEntryLegacy = Utils.GetDataCsvariantsAttribute((JsonObject)null, "x"); + JsonObject nullEntryCanonical = Utils.GetVariantMetadataTags((JsonObject)null, "x"); + Assert.True(JsonNode.DeepEquals(nullEntryCanonical, nullEntryLegacy)); - JObject nullArrLegacy = Utils.GetDataCsvariantsAttribute((JArray)null, "x"); - JObject nullArrCanonical = Utils.GetVariantMetadataTags((JArray)null, "x"); - Assert.True(JToken.DeepEquals(nullArrCanonical, nullArrLegacy)); + JsonObject nullArrLegacy = Utils.GetDataCsvariantsAttribute((JsonArray)null, "x"); + JsonObject nullArrCanonical = Utils.GetVariantMetadataTags((JsonArray)null, "x"); + Assert.True(JsonNode.DeepEquals(nullArrCanonical, nullArrLegacy)); #pragma warning restore CS0618 } [Fact] public void GetVariantAliases_ReturnsEmptyVariantsWhenPublishDetailsMissing() { - var entry = new JObject { ["uid"] = "blt_no_pd" }; - JObject result = Utils.GetVariantAliases(entry, "movie"); + var entry = new JsonObject { ["uid"] = "blt_no_pd" }; + JsonObject result = Utils.GetVariantAliases(entry, "movie"); Assert.Equal("blt_no_pd", result["entry_uid"].ToString()); Assert.Equal("movie", result["contenttype_uid"].ToString()); - Assert.Empty((JArray)result["variants"]); + Assert.Empty(result["variants"].AsArray()); } [Fact] public void GetVariantAliases_ReturnsEmptyVariantsWhenVariantsObjectEmpty() { - var entry = new JObject + var entry = new JsonObject { ["uid"] = "blt_empty_v", - ["publish_details"] = new JObject + ["publish_details"] = new JsonObject { - ["variants"] = new JObject() + ["variants"] = new JsonObject() } }; - JObject result = Utils.GetVariantAliases(entry, "movie"); - Assert.Empty((JArray)result["variants"]); + JsonObject result = Utils.GetVariantAliases(entry, "movie"); + Assert.Empty(result["variants"].AsArray()); } [Fact] public void GetVariantAliases_ReturnsEmptyVariantsWhenVariantsKeyMissing() { - var entry = new JObject + var entry = new JsonObject { ["uid"] = "blt_no_variants_key", - ["publish_details"] = new JObject { ["time"] = "2025-01-01T00:00:00.000Z" } + ["publish_details"] = new JsonObject { ["time"] = "2025-01-01T00:00:00.000Z" } }; - JObject result = Utils.GetVariantAliases(entry, "movie"); - Assert.Empty((JArray)result["variants"]); + JsonObject result = Utils.GetVariantAliases(entry, "movie"); + Assert.Empty(result["variants"].AsArray()); } [Fact] public void GetVariantAliases_SkipsVariantWhenAliasMissingOrEmpty() { - var entry = new JObject + var entry = new JsonObject { ["uid"] = "blt_skip", - ["publish_details"] = new JObject + ["publish_details"] = new JsonObject { - ["variants"] = new JObject + ["variants"] = new JsonObject { - ["v1"] = new JObject { ["alias"] = "keep_me" }, - ["v2"] = new JObject(), - ["v3"] = new JObject { ["alias"] = "" } + ["v1"] = new JsonObject { ["alias"] = "keep_me" }, + ["v2"] = new JsonObject(), + ["v3"] = new JsonObject { ["alias"] = "" } } } }; - JObject result = Utils.GetVariantAliases(entry, "page"); - var variants = (JArray)result["variants"]; + JsonObject result = Utils.GetVariantAliases(entry, "page"); + var variants = result["variants"].AsArray(); Assert.Single(variants); Assert.Equal("keep_me", variants[0].ToString()); } diff --git a/Contentstack.Utils/Contentstack.Utils.csproj b/Contentstack.Utils/Contentstack.Utils.csproj index 6eaeb87..881b6eb 100644 --- a/Contentstack.Utils/Contentstack.Utils.csproj +++ b/Contentstack.Utils/Contentstack.Utils.csproj @@ -2,6 +2,7 @@ netstandard2.0;net47;net472; + latest contentstack.utils Contentstack.Utils Contentstack @@ -40,6 +41,6 @@ - + diff --git a/Contentstack.Utils/Converters/NodeJsonConverter.cs b/Contentstack.Utils/Converters/NodeJsonConverter.cs index 88d22a7..2619eae 100644 --- a/Contentstack.Utils/Converters/NodeJsonConverter.cs +++ b/Contentstack.Utils/Converters/NodeJsonConverter.cs @@ -1,31 +1,91 @@ using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; using Contentstack.Utils.Models; namespace Contentstack.Utils.Converters { public class NodeJsonConverter : JsonConverter { - public override Node ReadJson(JsonReader reader, Type objectType, Node existingValue, bool hasExistingValue, JsonSerializer serializer) + public override Node Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - Node node = null; - JObject jObject = JObject.Load(reader); - if (jObject["type"] == null) + using (JsonDocument doc = JsonDocument.ParseValue(ref reader)) { - node = new TextNode(); - node.type = "text"; - }else - { - node = new Node(); + JsonElement root = doc.RootElement; + bool hasType = root.TryGetProperty("type", out JsonElement typeEl) && typeEl.ValueKind != JsonValueKind.Null; + + if (!hasType) + { + TextNode textNode = new TextNode(); + textNode.type = "text"; + PopulateTextNode(root, textNode, options); + return textNode; + } + + Node node = new Node(); + PopulateNode(root, node, options); + return node; } - serializer.Populate(jObject.CreateReader(), node); - return node; } - public override void WriteJson(JsonWriter writer, Node value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, Node value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value?.GetType() ?? typeof(Node), CloneWithoutNodeConverter(options)); + } + + private static void PopulateNode(JsonElement root, Node node, JsonSerializerOptions options) + { + if (root.TryGetProperty("type", out JsonElement typeProp)) + node.type = typeProp.GetString(); + + if (root.TryGetProperty("attrs", out JsonElement attrs)) + node.attrs = JsonSerializer.Deserialize>(attrs.GetRawText(), options); + + if (root.TryGetProperty("children", out JsonElement children)) + node.children = JsonSerializer.Deserialize>(children.GetRawText(), options); + } + + private static void PopulateTextNode(JsonElement root, TextNode node, JsonSerializerOptions options) + { + PopulateNode(root, node, options); + + if (root.TryGetProperty("text", out JsonElement text)) + node.text = text.GetString(); + + TryBindBool(root, "bold", v => node.bold = v); + TryBindBool(root, "italic", v => node.italic = v); + TryBindBool(root, "underline", v => node.underline = v); + TryBindBool(root, "strikethrough", v => node.strikethrough = v); + TryBindBool(root, "inlineCode", v => node.inlineCode = v); + TryBindBool(root, "subscript", v => node.subscript = v); + TryBindBool(root, "superscript", v => node.superscript = v); + + if (root.TryGetProperty("classname", out JsonElement cn)) + node.classname = cn.ValueKind == JsonValueKind.Null ? null : cn.GetString(); + + if (root.TryGetProperty("id", out JsonElement id)) + node.id = id.ValueKind == JsonValueKind.Null ? null : id.GetString(); + } + + private static void TryBindBool(JsonElement root, string name, Action set) { - + if (!root.TryGetProperty(name, out JsonElement el)) + return; + if (el.ValueKind == JsonValueKind.True || el.ValueKind == JsonValueKind.False) + set(el.GetBoolean()); + } + + private static JsonSerializerOptions CloneWithoutNodeConverter(JsonSerializerOptions options) + { + JsonSerializerOptions inner = new JsonSerializerOptions(options); + for (int i = inner.Converters.Count - 1; i >= 0; i--) + { + if (inner.Converters[i] is NodeJsonConverter) + inner.Converters.RemoveAt(i); + } + + return inner; } } } diff --git a/Contentstack.Utils/Converters/RTEJsonConverter.cs b/Contentstack.Utils/Converters/RTEJsonConverter.cs index ecc198b..6b57279 100644 --- a/Contentstack.Utils/Converters/RTEJsonConverter.cs +++ b/Contentstack.Utils/Converters/RTEJsonConverter.cs @@ -1,50 +1,169 @@ using System; using System.Linq; using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Contentstack.Utils.Constants; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Contentstack.Utils.Interfaces; namespace Contentstack.Utils.Converters { - public class RTEJsonConverter : JsonConverter + /// + /// Factory for JSON path-based deserialization used by and . + /// + public sealed class RTEJsonConverterFactory : JsonConverterFactory { - public override bool CanConvert(Type objectType) + public override bool CanConvert(Type typeToConvert) { - throw new InvalidOperationException(ErrorMessages.InvalidRteJson); + if (!typeToConvert.IsGenericType) + return false; + + Type def = typeToConvert.GetGenericTypeDefinition(); + return def == typeof(Models.JsonRTENode<>) || def == typeof(Models.JsonRTENodes<>); } - public override object ReadJson(JsonReader reader, Type objectType, - object existingValue, JsonSerializer serializer) + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - JObject jo = JObject.Load(reader); - object targetObj = Activator.CreateInstance(objectType); + Type embeddedArg = typeToConvert.GetGenericArguments()[0]; + Type def = typeToConvert.GetGenericTypeDefinition(); + + if (def == typeof(Models.JsonRTENode<>)) + { + Type converterType = typeof(RTEJsonConverterImpl<>).MakeGenericType(embeddedArg); + return (JsonConverter)Activator.CreateInstance(converterType); + } - foreach (PropertyInfo prop in objectType.GetProperties() - .Where(p => p.CanRead && p.CanWrite)) + if (def == typeof(Models.JsonRTENodes<>)) { - JsonPropertyAttribute att = prop.GetCustomAttributes(true) - .OfType() - .FirstOrDefault(); + Type converterType = typeof(RTEJsonNodesConverterImpl<>).MakeGenericType(embeddedArg); + return (JsonConverter)Activator.CreateInstance(converterType); + } - string jsonPath = (att != null ? att.PropertyName : prop.Name); - JToken token = jo.SelectToken(jsonPath); + throw new InvalidOperationException($"Unsupported type for RTE JSON converter: {typeToConvert}"); + } + } - if (token != null && token.Type != JTokenType.Null) + /// + /// Deserializes objects whose properties map to dotted JSON paths (same idea as Newtonsoft SelectToken with ). + /// + public sealed class PathMappedJsonConverter : JsonConverter + { + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + JsonObject jo = JsonNode.Parse(ref reader) as JsonObject ?? new JsonObject(); + T targetObj = Activator.CreateInstance(); + + foreach (PropertyInfo prop in typeof(T).GetProperties().Where(p => p.CanRead && p.CanWrite)) + { + JsonPropertyNameAttribute att = prop.GetCustomAttribute(); + string jsonPath = att?.Name ?? prop.Name; + JsonNode token = RTEJsonPath.SelectPath(jo, jsonPath); + + if (token != null && !RTEJsonPath.IsJsonNull(token)) { - object value = token.ToObject(prop.PropertyType, serializer); - prop.SetValue(targetObj, value, null); + object value = RTEJsonPath.DeserializeNode(token, prop.PropertyType, options); + prop.SetValue(targetObj, value); } } return targetObj; } + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + throw new NotSupportedException("Serialization is not supported for path-mapped RTE models."); + } + } - public override void WriteJson(JsonWriter writer, object value, - JsonSerializer serializer) + internal static class RTEJsonPath + { + internal static JsonNode SelectPath(JsonObject root, string path) + { + if (root == null || string.IsNullOrEmpty(path)) + return null; + + string[] segments = path.Split('.'); + JsonNode current = root; + foreach (string segment in segments) + { + if (current is JsonObject jo && jo.TryGetPropertyValue(segment, out JsonNode next)) + current = next; + else + return null; + } + + return current; + } + + internal static bool IsJsonNull(JsonNode node) { + if (node == null) + return true; + return node is JsonValue jv && jv.GetValueKind() == JsonValueKind.Null; + } + internal static object DeserializeNode(JsonNode token, Type propertyType, JsonSerializerOptions options) + { + string json = token.ToJsonString(options); + return JsonSerializer.Deserialize(json, propertyType, options); + } + } + + internal sealed class RTEJsonConverterImpl : JsonConverter> where T : IEmbeddedObject + { + public override Models.JsonRTENode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + JsonObject jo = JsonNode.Parse(ref reader) as JsonObject ?? new JsonObject(); + var targetObj = new Models.JsonRTENode(); + + foreach (PropertyInfo prop in typeof(Models.JsonRTENode).GetProperties().Where(p => p.CanRead && p.CanWrite)) + { + JsonPropertyNameAttribute att = prop.GetCustomAttribute(); + string jsonPath = att?.Name ?? prop.Name; + JsonNode token = RTEJsonPath.SelectPath(jo, jsonPath); + + if (token != null && !RTEJsonPath.IsJsonNull(token)) + { + object value = RTEJsonPath.DeserializeNode(token, prop.PropertyType, options); + prop.SetValue(targetObj, value); + } + } + + return targetObj; + } + + public override void Write(Utf8JsonWriter writer, Models.JsonRTENode value, JsonSerializerOptions options) + { + throw new NotSupportedException("Serialization is not supported for JsonRTENode."); + } + } + + internal sealed class RTEJsonNodesConverterImpl : JsonConverter> where T : IEmbeddedObject + { + public override Models.JsonRTENodes Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + JsonObject jo = JsonNode.Parse(ref reader) as JsonObject ?? new JsonObject(); + var targetObj = new Models.JsonRTENodes(); + + foreach (PropertyInfo prop in typeof(Models.JsonRTENodes).GetProperties().Where(p => p.CanRead && p.CanWrite)) + { + JsonPropertyNameAttribute att = prop.GetCustomAttribute(); + string jsonPath = att?.Name ?? prop.Name; + JsonNode token = RTEJsonPath.SelectPath(jo, jsonPath); + + if (token != null && !RTEJsonPath.IsJsonNull(token)) + { + object val = RTEJsonPath.DeserializeNode(token, prop.PropertyType, options); + prop.SetValue(targetObj, val); + } + } + + return targetObj; + } + + public override void Write(Utf8JsonWriter writer, Models.JsonRTENodes value, JsonSerializerOptions options) + { + throw new NotSupportedException("Serialization is not supported for JsonRTENodes."); } } } diff --git a/Contentstack.Utils/Interfaces/IEdges.cs b/Contentstack.Utils/Interfaces/IEdges.cs index a127b6b..bd0ccce 100644 --- a/Contentstack.Utils/Interfaces/IEdges.cs +++ b/Contentstack.Utils/Interfaces/IEdges.cs @@ -1,11 +1,11 @@ using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Contentstack.Utils.Interfaces { public class IEdges where T: IEmbeddedObject { - [JsonProperty("node")] + [JsonPropertyName("node")] public T Node { get; diff --git a/Contentstack.Utils/JsonAttrValue.cs b/Contentstack.Utils/JsonAttrValue.cs new file mode 100644 index 0000000..477239b --- /dev/null +++ b/Contentstack.Utils/JsonAttrValue.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Contentstack.Utils +{ + /// + /// Newtonsoft.Json deserialized scalar attributes as . + /// System.Text.Json stores dynamic values as — unwrap for the same runtime behavior. + /// + internal static class JsonAttrValue + { + internal static string AsString(object value) + { + if (value == null) + return null; + + if (value is string s) + return s; + + if (value is JsonElement je) + { + switch (je.ValueKind) + { + case JsonValueKind.String: + return je.GetString(); + case JsonValueKind.Number: + return je.GetRawText(); + case JsonValueKind.True: + return "true"; + case JsonValueKind.False: + return "false"; + case JsonValueKind.Null: + return null; + default: + return je.ToString(); + } + } + + return Convert.ToString(value); + } + + internal static bool TryGetStyleObject(object styleVal, out Dictionary styles) + { + styles = null; + if (styleVal == null) + return false; + + if (styleVal is JsonObject jo) + { + styles = JsonSerializer.Deserialize>(jo.ToJsonString()); + return styles != null; + } + + if (styleVal is JsonElement je && je.ValueKind == JsonValueKind.Object) + { + styles = JsonSerializer.Deserialize>(je.GetRawText()); + return styles != null; + } + + return false; + } + } +} diff --git a/Contentstack.Utils/Models/JsonRTENode.cs b/Contentstack.Utils/Models/JsonRTENode.cs index b69a1bc..068f573 100644 --- a/Contentstack.Utils/Models/JsonRTENode.cs +++ b/Contentstack.Utils/Models/JsonRTENode.cs @@ -1,17 +1,16 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Contentstack.Utils.Converters; using Contentstack.Utils.Interfaces; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Contentstack.Utils.Models { - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] - public class JsonRTENode where T: IEmbeddedObject + [JsonConverter(typeof(RTEJsonConverterFactory))] + public class JsonRTENode where T : IEmbeddedObject { - [JsonProperty("json")] + [JsonPropertyName("json")] public Node Json { get; set; } - [JsonProperty("embedded_itemsConnection.edges")] + [JsonPropertyName("embedded_itemsConnection.edges")] public List> Edges { get; set; } } } diff --git a/Contentstack.Utils/Models/JsonRTENodes.cs b/Contentstack.Utils/Models/JsonRTENodes.cs index 31dbfcd..538115e 100644 --- a/Contentstack.Utils/Models/JsonRTENodes.cs +++ b/Contentstack.Utils/Models/JsonRTENodes.cs @@ -1,17 +1,16 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Contentstack.Utils.Converters; using Contentstack.Utils.Interfaces; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Contentstack.Utils.Models { - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] + [JsonConverter(typeof(RTEJsonConverterFactory))] public class JsonRTENodes where T : IEmbeddedObject { - [JsonProperty("json")] + [JsonPropertyName("json")] public List Json { get; set; } - [JsonProperty("embedded_itemsConnection.edges")] + [JsonPropertyName("embedded_itemsConnection.edges")] public List> Edges { get; set; } } } diff --git a/Contentstack.Utils/Models/Metadata.cs b/Contentstack.Utils/Models/Metadata.cs index e87ee17..d29e385 100644 --- a/Contentstack.Utils/Models/Metadata.cs +++ b/Contentstack.Utils/Models/Metadata.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Runtime.CompilerServices; +using Contentstack.Utils; using Contentstack.Utils.Enums; using HtmlAgilityPack; @@ -71,13 +72,13 @@ public static implicit operator Metadata(HtmlNode node) public static implicit operator Metadata(Node node) { StyleType styleType; - if (!node.attrs.ContainsKey("display-type") || !(Enum.TryParse((string)node.attrs["display-type"], true, out styleType))) + if (!node.attrs.ContainsKey("display-type") || !(Enum.TryParse(JsonAttrValue.AsString(node.attrs["display-type"]), true, out styleType))) { styleType = StyleType.Block; } EmbedItemType embedItemType; - if (!node.attrs.ContainsKey("type") || !(Enum.TryParse((string)node.attrs["type"], true, out embedItemType))) + if (!node.attrs.ContainsKey("type") || !(Enum.TryParse(JsonAttrValue.AsString(node.attrs["type"]), true, out embedItemType))) { embedItemType = EmbedItemType.Entry; } @@ -89,10 +90,10 @@ public static implicit operator Metadata(Node node) string itemUID = ""; if (node.attrs.ContainsKey("entry-uid")) { - itemUID = (string)node.attrs["entry-uid"]; + itemUID = JsonAttrValue.AsString(node.attrs["entry-uid"]); }else if (node.attrs.ContainsKey("asset-uid")) { - itemUID = (string)node.attrs["asset-uid"]; + itemUID = JsonAttrValue.AsString(node.attrs["asset-uid"]); } return new Metadata() @@ -102,7 +103,7 @@ public static implicit operator Metadata(Node node) StyleType = styleType, ItemType = embedItemType, ItemUid = itemUID, - ContentTypeUid = node.attrs.ContainsKey("content-type-uid") ? (string)node.attrs["content-type-uid"] : "", + ContentTypeUid = node.attrs.ContainsKey("content-type-uid") ? JsonAttrValue.AsString(node.attrs["content-type-uid"]) : "", attributes = node.attrs }; diff --git a/Contentstack.Utils/Models/Node.cs b/Contentstack.Utils/Models/Node.cs index b64a70e..ba7e659 100644 --- a/Contentstack.Utils/Models/Node.cs +++ b/Contentstack.Utils/Models/Node.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Contentstack.Utils.Converters; -using Contentstack.Utils.Enums; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json.Serialization; namespace Contentstack.Utils.Models { diff --git a/Contentstack.Utils/Models/Options.cs b/Contentstack.Utils/Models/Options.cs index 724322a..d8433a6 100644 --- a/Contentstack.Utils/Models/Options.cs +++ b/Contentstack.Utils/Models/Options.cs @@ -2,7 +2,8 @@ using System.Collections.Generic; using Contentstack.Utils.Enums; using Contentstack.Utils.Interfaces; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using Contentstack.Utils; namespace Contentstack.Utils.Models { @@ -105,14 +106,12 @@ public virtual string RenderNode(string nodeType, Node node, NodeChildrenCallBac var styleVal = node.attrs["style"]; if (styleVal != null) { - if (styleVal is string) + if (styleVal is string styleStr) { - styleAttrs = $" style=\"{styleVal}\""; + styleAttrs = $" style=\"{styleStr}\""; } - else if (styleVal is JObject) + else if (JsonAttrValue.TryGetStyleObject(styleVal, out Dictionary styleDictionary) && styleDictionary != null) { - var styleObject = (JObject)styleVal; - var styleDictionary = styleObject.ToObject>(); styleAttrs = " style=\""; foreach (var pair in styleDictionary) { @@ -129,28 +128,28 @@ public virtual string RenderNode(string nodeType, Node node, NodeChildrenCallBac case "a": if (node.attrs?.ContainsKey("url")==true) { - href = (string)node.attrs["url"]; + href = JsonAttrValue.AsString(node.attrs["url"]); } if (node.attrs?.ContainsKey("target") == true) { - target = (string)node.attrs["target"]; + target = JsonAttrValue.AsString(node.attrs["target"]); } if (node.attrs?.ContainsKey("title") == true) { - title = (string)node.attrs["title"]; + title = JsonAttrValue.AsString(node.attrs["title"]); } return $"{callBack(node.children)}"; case "img": if (node.attrs.ContainsKey("url")) { - href = (string)node.attrs["url"]; + href = JsonAttrValue.AsString(node.attrs["url"]); } return $"{callBack(node.children)}"; case "embed": if (node.attrs.ContainsKey("url")) { - href = (string)node.attrs["url"]; + href = JsonAttrValue.AsString(node.attrs["url"]); } return $"{callBack(node.children)}"; case "fragment": diff --git a/Contentstack.Utils/Utils.cs b/Contentstack.Utils/Utils.cs index 4e51c19..fdd51be 100644 --- a/Contentstack.Utils/Utils.cs +++ b/Contentstack.Utils/Utils.cs @@ -5,8 +5,8 @@ using Contentstack.Utils.Interfaces; using System; using Contentstack.Utils.Enums; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; namespace Contentstack.Utils { @@ -289,7 +289,16 @@ private static object GetParentTagsValue(string dataValue, bool tagsAsObject) } } - public static JObject GetVariantAliases(JObject entry, string contentTypeUid) + private static readonly JsonSerializerOptions CompactJsonOptions = new JsonSerializerOptions { WriteIndented = false }; + + private static bool IsJsonNull(JsonNode node) + { + if (node == null) + return true; + return node is JsonValue jv && jv.GetValueKind() == JsonValueKind.Null; + } + + public static JsonObject GetVariantAliases(JsonObject entry, string contentTypeUid) { if (string.IsNullOrEmpty(contentTypeUid)) { @@ -299,14 +308,14 @@ public static JObject GetVariantAliases(JObject entry, string contentTypeUid) { throw new ArgumentException("Entry must not be null."); } - if (!entry.ContainsKey("uid") || entry["uid"] == null || entry["uid"].Type == JTokenType.Null) + if (!entry.TryGetPropertyValue("uid", out JsonNode uidNode) || IsJsonNull(uidNode)) { throw new ArgumentException("Entry must contain uid."); } - string entryUid = entry["uid"]?.ToString() ?? ""; - JArray variantsArray = ExtractVariantAliasesFromEntry(entry); - JObject result = new JObject + string entryUid = uidNode?.ToString() ?? ""; + JsonArray variantsArray = ExtractVariantAliasesFromEntry(entry); + var result = new JsonObject { ["entry_uid"] = entryUid, ["contenttype_uid"] = contentTypeUid, @@ -315,7 +324,7 @@ public static JObject GetVariantAliases(JObject entry, string contentTypeUid) return result; } - public static JArray GetVariantAliases(JArray entries, string contentTypeUid) + public static JsonArray GetVariantAliases(JsonArray entries, string contentTypeUid) { if (string.IsNullOrEmpty(contentTypeUid)) { @@ -323,13 +332,15 @@ public static JArray GetVariantAliases(JArray entries, string contentTypeUid) } if (entries == null) { - return new JArray(); + return new JsonArray(); } - JArray variantResults = new JArray(); - foreach (JToken token in entries) + var variantResults = new JsonArray(); + foreach (JsonNode token in entries) { - JObject entry = token as JObject; - if (entry != null && entry.ContainsKey("uid") && entry["uid"] != null && entry["uid"].Type != JTokenType.Null) + if (token is JsonObject entry && + entry.TryGetPropertyValue("uid", out JsonNode uidNode) && + uidNode != null && + !IsJsonNull(uidNode)) { variantResults.Add(GetVariantAliases(entry, contentTypeUid)); } @@ -342,17 +353,17 @@ public static JArray GetVariantAliases(JArray entries, string contentTypeUid) /// /// Entry JSON (e.g. from the Delivery API), or null to produce an empty payload. /// Content type UID for the entry. - /// A with a data-csvariants key whose value is a compact JSON array string. - public static JObject GetVariantMetadataTags(JObject entry, string contentTypeUid) + /// A with a data-csvariants key whose value is a compact JSON array string. + public static JsonObject GetVariantMetadataTags(JsonObject entry, string contentTypeUid) { if (entry == null) { - JObject result = new JObject(); - result["data-csvariants"] = "[]"; - return result; + var empty = new JsonObject(); + empty["data-csvariants"] = "[]"; + return empty; } - JArray entries = new JArray(); - entries.Add(entry); + var entries = new JsonArray(); + entries.Add(entry.DeepClone()); return GetVariantMetadataTags(entries, contentTypeUid); } @@ -361,10 +372,10 @@ public static JObject GetVariantMetadataTags(JObject entry, string contentTypeUi /// /// Array of entry JSON objects, or null to produce an empty payload. /// Content type UID shared by these entries. - /// A with a data-csvariants key whose value is a compact JSON array string. - public static JObject GetVariantMetadataTags(JArray entries, string contentTypeUid) + /// A with a data-csvariants key whose value is a compact JSON array string. + public static JsonObject GetVariantMetadataTags(JsonArray entries, string contentTypeUid) { - JObject result = new JObject(); + var result = new JsonObject(); if (entries == null) { result["data-csvariants"] = "[]"; @@ -375,46 +386,44 @@ public static JObject GetVariantMetadataTags(JArray entries, string contentTypeU throw new ArgumentException("ContentType is required."); } - JArray variantResults = GetVariantAliases(entries, contentTypeUid); - result["data-csvariants"] = variantResults.ToString(Formatting.None); + JsonArray variantResults = GetVariantAliases(entries, contentTypeUid); + result["data-csvariants"] = variantResults.ToJsonString(CompactJsonOptions); return result; } /// - /// Prefer . This alias exists for backward compatibility and will be removed in a future major release. + /// Prefer . This alias exists for backward compatibility and will be removed in a future major release. /// [Obsolete("Use GetVariantMetadataTags instead. This method will be removed in a future major release.")] - public static JObject GetDataCsvariantsAttribute(JObject entry, string contentTypeUid) + public static JsonObject GetDataCsvariantsAttribute(JsonObject entry, string contentTypeUid) { return GetVariantMetadataTags(entry, contentTypeUid); } /// - /// Prefer . This alias exists for backward compatibility and will be removed in a future major release. + /// Prefer . This alias exists for backward compatibility and will be removed in a future major release. /// [Obsolete("Use GetVariantMetadataTags instead. This method will be removed in a future major release.")] - public static JObject GetDataCsvariantsAttribute(JArray entries, string contentTypeUid) + public static JsonObject GetDataCsvariantsAttribute(JsonArray entries, string contentTypeUid) { return GetVariantMetadataTags(entries, contentTypeUid); } - private static JArray ExtractVariantAliasesFromEntry(JObject entry) + private static JsonArray ExtractVariantAliasesFromEntry(JsonObject entry) { - JArray variantArray = new JArray(); - JObject publishDetails = entry["publish_details"] as JObject; - if (publishDetails == null) + var variantArray = new JsonArray(); + if (!entry.TryGetPropertyValue("publish_details", out JsonNode pdNode) || pdNode is not JsonObject publishDetails) { return variantArray; } - JObject variants = publishDetails["variants"] as JObject; - if (variants == null) + if (!publishDetails.TryGetPropertyValue("variants", out JsonNode vNode) || vNode is not JsonObject variants) { return variantArray; } - foreach (JProperty prop in variants.Properties()) + foreach (KeyValuePair kvp in variants) { - if (prop.Value is JObject valueObj) + if (kvp.Value is JsonObject valueObj) { string alias = valueObj["alias"]?.ToString(); if (!string.IsNullOrEmpty(alias)) diff --git a/Directory.Build.props b/Directory.Build.props index 8bdd5df..d6ba3f1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 1.2.0 + 2.0.0-beta.1