From 9289d273a13dab259e99cafc2f3a48372cf98ff5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:59:21 +0000 Subject: [PATCH 1/7] Initial plan From c8467ca6ebbea7d420cfa0f4fca3cb8e63a42e60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:07:30 +0000 Subject: [PATCH 2/7] Add JSON serialization helper infrastructure for System.Text.Json migration Co-authored-by: QilongTang <3942418+QilongTang@users.noreply.github.com> --- .../Serialization/JsonSerializationHelper.cs | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 src/DynamoCore/Serialization/JsonSerializationHelper.cs diff --git a/src/DynamoCore/Serialization/JsonSerializationHelper.cs b/src/DynamoCore/Serialization/JsonSerializationHelper.cs new file mode 100644 index 00000000000..8beca39de11 --- /dev/null +++ b/src/DynamoCore/Serialization/JsonSerializationHelper.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Dynamo.Serialization +{ + /// + /// Helper class for JSON serialization using System.Text.Json. + /// Provides utilities to replace Newtonsoft.Json functionality. + /// + public static class JsonSerializationHelper + { + /// + /// Creates default JsonSerializerOptions for Dynamo serialization. + /// + /// Optional custom converters to include + /// Configured JsonSerializerOptions + public static JsonSerializerOptions CreateSerializerOptions(params JsonConverter[] converters) + { + var options = new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + ReferenceHandler = ReferenceHandler.IgnoreCycles, + PropertyNameCaseInsensitive = false, + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + // Use invariant culture for consistent serialization + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + // Add custom converters + if (converters != null) + { + foreach (var converter in converters) + { + options.Converters.Add(converter); + } + } + + return options; + } + + /// + /// Creates JsonSerializerOptions for deserialization with backward compatibility. + /// + /// Optional custom converters to include + /// Configured JsonSerializerOptions + public static JsonSerializerOptions CreateDeserializerOptions(params JsonConverter[] converters) + { + var options = new JsonSerializerOptions + { + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + ReferenceHandler = ReferenceHandler.IgnoreCycles, + PropertyNameCaseInsensitive = true, // More lenient for reading old files + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip + }; + + // Add custom converters + if (converters != null) + { + foreach (var converter in converters) + { + options.Converters.Add(converter); + } + } + + return options; + } + + /// + /// Safely gets a string value from a JsonElement. + /// + public static string GetStringOrDefault(JsonElement element, string defaultValue = "") + { + return element.ValueKind == JsonValueKind.String ? element.GetString() ?? defaultValue : defaultValue; + } + + /// + /// Safely gets an int value from a JsonElement. + /// + public static int GetInt32OrDefault(JsonElement element, int defaultValue = 0) + { + return element.ValueKind == JsonValueKind.Number && element.TryGetInt32(out var value) ? value : defaultValue; + } + + /// + /// Safely gets a double value from a JsonElement. + /// + public static double GetDoubleOrDefault(JsonElement element, double defaultValue = 0.0) + { + return element.ValueKind == JsonValueKind.Number && element.TryGetDouble(out var value) ? value : defaultValue; + } + + /// + /// Safely gets a bool value from a JsonElement. + /// + public static bool GetBooleanOrDefault(JsonElement element, bool defaultValue = false) + { + if (element.ValueKind == JsonValueKind.True) return true; + if (element.ValueKind == JsonValueKind.False) return false; + return defaultValue; + } + + /// + /// Safely gets a Guid value from a JsonElement. + /// + public static Guid GetGuidOrDefault(JsonElement element, Guid defaultValue = default) + { + if (element.ValueKind == JsonValueKind.String) + { + var str = element.GetString(); + if (Guid.TryParse(str, out var guid)) + { + return guid; + } + } + return defaultValue; + } + + /// + /// Tries to get a property from a JsonElement. + /// + public static bool TryGetProperty(JsonElement element, string propertyName, out JsonElement property) + { + if (element.ValueKind == JsonValueKind.Object) + { + return element.TryGetProperty(propertyName, out property); + } + property = default; + return false; + } + + /// + /// Gets an array of JsonElements from a property, or empty array if not found. + /// + public static JsonElement[] GetArrayOrEmpty(JsonElement element, string propertyName) + { + if (TryGetProperty(element, propertyName, out var property) && property.ValueKind == JsonValueKind.Array) + { + var list = new List(); + foreach (var item in property.EnumerateArray()) + { + list.Add(item); + } + return list.ToArray(); + } + return Array.Empty(); + } + + /// + /// Deserializes a JsonElement to a specific type using the provided options. + /// + public static T Deserialize(JsonElement element, JsonSerializerOptions options = null) + { + var json = element.GetRawText(); + return JsonSerializer.Deserialize(json, options); + } + + /// + /// Parses a JSON string and returns a JsonDocument. + /// The caller is responsible for disposing the returned JsonDocument. + /// + public static JsonDocument ParseJson(string json) + { + return JsonDocument.Parse(json); + } + + /// + /// Writes a JSON value with error handling. + /// + public static void WriteValue(Utf8JsonWriter writer, string propertyName, string value) + { + writer.WriteString(propertyName, value); + } + + /// + /// Writes a JSON value with error handling. + /// + public static void WriteValue(Utf8JsonWriter writer, string propertyName, int value) + { + writer.WriteNumber(propertyName, value); + } + + /// + /// Writes a JSON value with error handling. + /// + public static void WriteValue(Utf8JsonWriter writer, string propertyName, double value) + { + writer.WriteNumber(propertyName, value); + } + + /// + /// Writes a JSON value with error handling. + /// + public static void WriteValue(Utf8JsonWriter writer, string propertyName, bool value) + { + writer.WriteBoolean(propertyName, value); + } + + /// + /// Writes a JSON value with error handling. + /// + public static void WriteValue(Utf8JsonWriter writer, string propertyName, Guid value) + { + writer.WriteString(propertyName, value.ToString()); + } + } +} From f4cadad730c22467097b44b233946b410d22f213 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:12:16 +0000 Subject: [PATCH 3/7] Migrate low-risk components to System.Text.Json (Phase 1) Co-authored-by: QilongTang <3942418+QilongTang@users.noreply.github.com> --- src/DynamoCoreWpf/UI/GuidedTour/CutOffArea.cs | 14 +++---- src/DynamoCoreWpf/UI/GuidedTour/ExitGuide.cs | 12 +++--- src/DynamoCoreWpf/UI/GuidedTour/Guide.cs | 10 ++--- .../UI/GuidedTour/GuidesManager.cs | 4 +- .../UI/GuidedTour/HightlightArea.cs | 16 ++++---- .../UI/GuidedTour/HostControlInfo.cs | 24 ++++++------ src/DynamoCoreWpf/UI/GuidedTour/Step.cs | 38 +++++++++---------- .../DynamoFeatureFlagsManager.cs | 25 ++++++++++-- .../NotificationCenterController.cs | 6 +-- 9 files changed, 84 insertions(+), 65 deletions(-) diff --git a/src/DynamoCoreWpf/UI/GuidedTour/CutOffArea.cs b/src/DynamoCoreWpf/UI/GuidedTour/CutOffArea.cs index b236e140859..b7ae53a8fbc 100644 --- a/src/DynamoCoreWpf/UI/GuidedTour/CutOffArea.cs +++ b/src/DynamoCoreWpf/UI/GuidedTour/CutOffArea.cs @@ -1,6 +1,6 @@ using System.Windows; using Dynamo.Core; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Dynamo.Wpf.UI.GuidedTour { @@ -20,37 +20,37 @@ public class CutOffArea : NotificationObject /// /// Since the box that cuts the elements has its size fixed, this variable applies a value to fix its Width /// - [JsonProperty(nameof(WidthBoxDelta))] + [JsonPropertyName(nameof(WidthBoxDelta))] public double WidthBoxDelta { get => widthBoxDelta; set => widthBoxDelta = value; } /// /// Since the box that cuts the elements has its size fixed, this variable applies a value to fix its Height /// - [JsonProperty(nameof(HeightBoxDelta))] + [JsonPropertyName(nameof(HeightBoxDelta))] public double HeightBoxDelta { get => heightBoxDelta; set => heightBoxDelta = value; } /// /// This property will move the CutOff area horizontally over the X axis /// - [JsonProperty(nameof(XPosOffset))] + [JsonPropertyName(nameof(XPosOffset))] public double XPosOffset { get => xPosOffset; set => xPosOffset = value; } /// /// This property will move the CutOff area vertically over the Y axis /// - [JsonProperty(nameof(YPosOffset))] + [JsonPropertyName(nameof(YPosOffset))] public double YPosOffset { get => yPosOffset; set => yPosOffset = value; } /// /// In the case the cutoff area is not the same than HostControlInfo.HostUIElementString the this property needs to be populated /// - [JsonProperty(nameof(WindowElementNameString))] + [JsonPropertyName(nameof(WindowElementNameString))] public string WindowElementNameString { get; set; } /// /// In cases when we need to put the CutOff area over a node in the Workspace this property will be used /// - [JsonProperty(nameof(NodeId))] + [JsonPropertyName(nameof(NodeId))] public string NodeId { get; set; } /// diff --git a/src/DynamoCoreWpf/UI/GuidedTour/ExitGuide.cs b/src/DynamoCoreWpf/UI/GuidedTour/ExitGuide.cs index 71b89e89ac9..07e3c63e98d 100644 --- a/src/DynamoCoreWpf/UI/GuidedTour/ExitGuide.cs +++ b/src/DynamoCoreWpf/UI/GuidedTour/ExitGuide.cs @@ -1,8 +1,8 @@ -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace Dynamo.Wpf.UI.GuidedTour @@ -12,25 +12,25 @@ internal class ExitGuide /// /// Represents the Height of Exit Guide modal /// - [JsonProperty("Height")] + [JsonPropertyName("Height")] public double Height { get; set; } /// /// Represents the Width of Exit Guide modal /// - [JsonProperty("Width")] + [JsonPropertyName("Width")] public double Width { get; set; } /// /// Represents the key to the resources related to the Title of Exit Guide modal /// - [JsonProperty("Title")] + [JsonPropertyName("Title")] public string Title { get; set; } /// /// Represents the formatted text key to the resources related to the Title of Exit Guide modal /// - [JsonProperty("FormattedText")] + [JsonPropertyName("FormattedText")] public string FormattedText { get; set; } } } diff --git a/src/DynamoCoreWpf/UI/GuidedTour/Guide.cs b/src/DynamoCoreWpf/UI/GuidedTour/Guide.cs index 874486afa6b..e8b73bc4c15 100644 --- a/src/DynamoCoreWpf/UI/GuidedTour/Guide.cs +++ b/src/DynamoCoreWpf/UI/GuidedTour/Guide.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Windows; using System.Windows.Controls; using System.Windows.Media; @@ -9,7 +10,6 @@ using Dynamo.ViewModels; using Dynamo.Wpf.Properties; using Dynamo.Wpf.Views.GuidedTour; -using Newtonsoft.Json; namespace Dynamo.Wpf.UI.GuidedTour { @@ -21,13 +21,13 @@ public class Guide /// /// This list will contain all the steps per guide read from a json file /// - [JsonProperty("GuideSteps")] + [JsonPropertyName("GuideSteps")] internal List GuideSteps { get; set; } /// /// This property represent the name of the Guide, e.g. "Get Started", "Packages" /// - [JsonProperty("Name")] + [JsonPropertyName("Name")] internal string Name { get; set; } /// @@ -36,13 +36,13 @@ public class Guide /// 1 - User interface guide /// 2 - Onboarding guide /// - [JsonProperty("SequenceOrder")] + [JsonPropertyName("SequenceOrder")] internal int SequenceOrder { get; set; } /// /// This property has the resource key string for the guide /// - [JsonProperty("GuideNameResource")] + [JsonPropertyName("GuideNameResource")] internal string GuideNameResource { get; set; } diff --git a/src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs b/src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs index deac15a406d..dbcfab1a4f6 100644 --- a/src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs +++ b/src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text.Json; using System.Windows; using System.Windows.Controls; using Dynamo.Controls; @@ -12,7 +13,6 @@ using Dynamo.Wpf.Properties; using Dynamo.Wpf.ViewModels.GuidedTour; using Dynamo.Wpf.Views.GuidedTour; -using Newtonsoft.Json; using Res = Dynamo.Wpf.Properties.Resources; namespace Dynamo.Wpf.UI.GuidedTour @@ -305,7 +305,7 @@ private static List ReadGuides(string jsonFile) } //Deserialize all the information read from the json file - return JsonConvert.DeserializeObject>(jsonString); + return JsonSerializer.Deserialize>(jsonString); } /// diff --git a/src/DynamoCoreWpf/UI/GuidedTour/HightlightArea.cs b/src/DynamoCoreWpf/UI/GuidedTour/HightlightArea.cs index 651a6a546d4..835953bb2a5 100644 --- a/src/DynamoCoreWpf/UI/GuidedTour/HightlightArea.cs +++ b/src/DynamoCoreWpf/UI/GuidedTour/HightlightArea.cs @@ -1,6 +1,6 @@ using System.Windows; using Dynamo.Core; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Dynamo.Wpf.UI.GuidedTour { @@ -39,20 +39,20 @@ internal HighlightArea(HighlightArea copyInstance) /// /// Since the box that highlights the elements has its size fixed, this variable applies a value to fix its Width /// - [JsonProperty(nameof(WidthBoxDelta))] + [JsonPropertyName(nameof(WidthBoxDelta))] public double WidthBoxDelta { get => widthBoxDelta; set => widthBoxDelta = value; } /// /// Since the box that highlights the elements has its size fixed, this variable applies a value to fix its Height /// - [JsonProperty(nameof(HeightBoxDelta))] + [JsonPropertyName(nameof(HeightBoxDelta))] public double HeightBoxDelta { get => heightBoxDelta; set => heightBoxDelta = value; } /// /// This property will highlight the clickable area if its set to true /// - [JsonProperty(nameof(HighlightColor))] + [JsonPropertyName(nameof(HighlightColor))] public string HighlightColor { get; set; } /// @@ -74,26 +74,26 @@ public UIElement WindowElementName /// /// This property will contain the Window name in case we need to highligh a control in a different Window than DynamoView /// - [JsonProperty(nameof(WindowName))] + [JsonPropertyName(nameof(WindowName))] public string WindowName { get; set; } /// /// This represent the UIElement as string of the VisualTree that will be highlighted (readed from the json file) /// - [JsonProperty(nameof(WindowElementNameString))] + [JsonPropertyName(nameof(WindowElementNameString))] public string WindowElementNameString { get; set; } /// /// This property represent the UI Element Type that contains the UIElement (WindowElementName) due that in some cases is not the DynamoView (like the sub MenuItems) /// - [JsonProperty(nameof(UIElementTypeString))] + [JsonPropertyName(nameof(UIElementTypeString))] public string UIElementTypeString { get; set; } /// /// In case the UIElement to be highlighted is in a Grid, then this property will have the Grid name /// - [JsonProperty(nameof(UIElementGridContainer))] + [JsonPropertyName(nameof(UIElementGridContainer))] public string UIElementGridContainer { get; set; } diff --git a/src/DynamoCoreWpf/UI/GuidedTour/HostControlInfo.cs b/src/DynamoCoreWpf/UI/GuidedTour/HostControlInfo.cs index 9661d2a710b..59ebab9aca5 100644 --- a/src/DynamoCoreWpf/UI/GuidedTour/HostControlInfo.cs +++ b/src/DynamoCoreWpf/UI/GuidedTour/HostControlInfo.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Windows; using System.Windows.Controls.Primitives; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Dynamo.Wpf.UI.GuidedTour { @@ -71,19 +71,19 @@ public UIElement HostUIElement /// /// This variable will hold the name of the host (UIElement) in a string representation /// - [JsonProperty("HostUIElementString")] + [JsonPropertyName("HostUIElementString")] public string HostUIElementString { get; set; } /// /// This property will hold the placement location of the popup, for now we are just using Right, Left, Top and Bottom /// - [JsonProperty("PopupPlacement")] + [JsonPropertyName("PopupPlacement")] public PlacementMode PopupPlacement { get; set; } /// /// Once the popup host control and placecement is set we can use this property for moving the popup location Vertically (by specifying an offset) /// - [JsonProperty("VerticalPopupOffset")] + [JsonPropertyName("VerticalPopupOffset")] public double VerticalPopupOffSet { get @@ -99,7 +99,7 @@ public double VerticalPopupOffSet /// /// Once the popup host control and placecement is set we can use this property for moving the popup location Horizontally (by specifying an offset) /// - [JsonProperty("HorizontalPopupOffset")] + [JsonPropertyName("HorizontalPopupOffset")] public double HorizontalPopupOffSet { get @@ -115,31 +115,31 @@ public double HorizontalPopupOffSet /// /// Property that represents the highlight rectangle that will be shown over the Overlay /// - [JsonProperty("HighlightRectArea")] + [JsonPropertyName("HighlightRectArea")] internal HighlightArea HighlightRectArea { get; set; } /// /// Property that represents the cut off section that will be removed from the Overlay /// - [JsonProperty("CutOffRectArea")] + [JsonPropertyName("CutOffRectArea")] internal CutOffArea CutOffRectArea { get; set; } /// /// The html page that is going to be rendered inside the popup /// - [JsonProperty("HtmlPage")] + [JsonPropertyName("HtmlPage")] public HtmlPage HtmlPage { get => htmlPage; set => htmlPage = value; } /// /// This property will contain the WPF Window Name for the cases when the Popup need to be located in a different Window than DynamoView /// - [JsonProperty(nameof(WindowName))] + [JsonPropertyName(nameof(WindowName))] internal string WindowName { get; set; } /// /// This property will decide if the Popup.PlacementTarget needs to be calculated again or not (probably after UI Automation a new Window was opened) /// - [JsonProperty(nameof(DynamicHostWindow))] + [JsonPropertyName(nameof(DynamicHostWindow))] internal bool DynamicHostWindow { get; set; } } @@ -151,13 +151,13 @@ public class HtmlPage /// /// A dictionary containing the key word to be replaced in page and the filename as values /// - [JsonProperty("Resources")] + [JsonPropertyName("Resources")] public Dictionary Resources { get => resources; set => resources = value; } /// /// Filename of the HTML page /// - [JsonProperty("FileName")] + [JsonPropertyName("FileName")] public string FileName { get => fileName; set => fileName = value; } } } diff --git a/src/DynamoCoreWpf/UI/GuidedTour/Step.cs b/src/DynamoCoreWpf/UI/GuidedTour/Step.cs index 29699acd26b..01fbbd1c16a 100644 --- a/src/DynamoCoreWpf/UI/GuidedTour/Step.cs +++ b/src/DynamoCoreWpf/UI/GuidedTour/Step.cs @@ -16,7 +16,7 @@ using Dynamo.ViewModels; using Dynamo.Wpf.Properties; using Dynamo.Wpf.Views.GuidedTour; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Dynamo.Wpf.UI.GuidedTour { @@ -57,73 +57,73 @@ public enum StepTypes{ SURVEY, TOOLTIP, WELCOME, EXIT_TOUR}; /// /// The step type will describe which type of window will be created after reading the json file, it can be SURVEY, TOOLTIP, WELCOME, EXIT_TOUR /// - [JsonProperty("Type")] + [JsonPropertyName("Type")] public StepTypes StepType { get; set; } /// /// The step content will contain the title and the popup content (included formatted text) /// - [JsonProperty("StepContent")] + [JsonPropertyName("StepContent")] public Content StepContent { get; set; } /// /// There are some specific Steps that will contain extra content (like Survey.RatingTextTitle), then this list will store the information /// - [JsonProperty("ExtraContent")] + [JsonPropertyName("ExtraContent")] public List StepExtraContent { get; set; } /// /// Step name, it just represent a step identifier /// - [JsonProperty("Name")] + [JsonPropertyName("Name")] public string Name { get; set; } /// /// When the Step Width property is not provided the default value will be 480 /// - [JsonProperty("Width")] + [JsonPropertyName("Width")] public double Width { get; set; } = 480; /// /// When the Step Height property is not provide the default value will be 190 /// - [JsonProperty("Height")] + [JsonPropertyName("Height")] public double Height { get; set; } = 190; /// /// Represent a sequencial numeric value for each step, when is a multiflow guide this value can be repeated /// - [JsonProperty("Sequence")] + [JsonPropertyName("Sequence")] public int Sequence { get; set; } = 0; /// /// This property contains the Host information like the host popup element or the popup position /// - [JsonProperty("HostPopupInfo")] + [JsonPropertyName("HostPopupInfo")] public HostControlInfo HostPopupInfo { get; set; } /// /// This property will hold a list of UI Automation actions (information) that will be executed when the Next or Back button are pressed /// - [JsonProperty("UIAutomation")] + [JsonPropertyName("UIAutomation")] public List UIAutomation { get; set; } /// /// This property will hold information about the methods/actions that should be executed before showing a Popup(Step) /// - [JsonProperty("PreValidation")] + [JsonPropertyName("PreValidation")] internal PreValidation PreValidationInfo { get; set; } /// /// This property will show the library if It's set to true /// - [JsonProperty("ShowLibrary")] + [JsonPropertyName("ShowLibrary")] public bool ShowLibrary { get; set; } /// /// This propertu will hold information about the exit guide modal /// - [JsonProperty("ExitGuide")] + [JsonPropertyName("ExitGuide")] internal ExitGuide ExitGuide { get; set; } public enum PointerDirection { TOP_RIGHT, TOP_LEFT, BOTTOM_RIGHT, BOTTOM_LEFT, BOTTOM_DOWN, NONE }; @@ -186,12 +186,12 @@ internal Popup StepUIPopup /// This property describe which will be the pointing direction of the tooltip (if is a Welcome or Survey popup then is not used) /// By default the tooltip pointer will be pointing to the left and will be located at the top /// - [JsonProperty("TooltipPointerDirection")] + [JsonPropertyName("TooltipPointerDirection")] public PointerDirection TooltipPointerDirection { get; set; } = PointerDirection.TOP_LEFT; /// /// A vertical offfset to the pointer of the popups /// - [JsonProperty("PointerVerticalOffset")] + [JsonPropertyName("PointerVerticalOffset")] public double PointerVerticalOffset { get; set; } #endregion @@ -868,13 +868,13 @@ public class Content /// /// Title of the Popup shown at the top-center /// - [JsonProperty("Title")] + [JsonPropertyName("Title")] public string Title { get; set; } /// /// The content of the Popup using a specific format for showing text, hyperlinks,images, bullet points in a RichTextBox /// - [JsonProperty("FormattedText")] + [JsonPropertyName("FormattedText")] public string FormattedText { get @@ -896,10 +896,10 @@ public string FormattedText /// public class ExtraContent { - [JsonProperty("Property")] + [JsonPropertyName("Property")] public string Property { get; set; } - [JsonProperty("Value")] + [JsonPropertyName("Value")] public string Value { get; set; } } } diff --git a/src/DynamoUtilities/DynamoFeatureFlagsManager.cs b/src/DynamoUtilities/DynamoFeatureFlagsManager.cs index 40765e493d0..9a00c7eef26 100644 --- a/src/DynamoUtilities/DynamoFeatureFlagsManager.cs +++ b/src/DynamoUtilities/DynamoFeatureFlagsManager.cs @@ -1,11 +1,11 @@ using Dynamo.Utilities; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Reflection; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -68,14 +68,33 @@ internal void CacheAllFlags() //convert from json string to dictionary. try { - AllFlagsCache = JsonConvert.DeserializeObject>(dataFromCLI); + // System.Text.Json deserializes numbers as JsonElement by default for Dictionary + // We need to convert them to the appropriate types + var rawCache = JsonSerializer.Deserialize>(dataFromCLI); + AllFlagsCache = new Dictionary(); + + foreach (var kvp in rawCache) + { + var element = kvp.Value; + object value = element.ValueKind switch + { + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.TryGetInt64(out var longVal) ? longVal : element.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + _ => element.ToString() + }; + AllFlagsCache[kvp.Key] = value; + } + // Invoke the flags retrieved event on the sync context which should be the main ui thread (if in Dynamo with UI) or the default SyncContext (if in headless mode). syncContext?.Send((_) => { FlagsRetrieved?.Invoke(); }, null); - var formattedFlags = JsonConvert.SerializeObject(AllFlagsCache, Formatting.Indented); + var formattedFlags = JsonSerializer.Serialize(AllFlagsCache, new JsonSerializerOptions { WriteIndented = true }); OnLogMessage($"retrieved feature flags with value: {formattedFlags}"); } diff --git a/src/Notifications/NotificationCenterController.cs b/src/Notifications/NotificationCenterController.cs index 312012a851e..4aa6393d8ce 100755 --- a/src/Notifications/NotificationCenterController.cs +++ b/src/Notifications/NotificationCenterController.cs @@ -6,6 +6,7 @@ using System.Net; using System.Reflection; using System.Runtime.InteropServices; +using System.Text.Json; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; @@ -15,7 +16,6 @@ using DynamoUtilities; using Dynamo.ViewModels; using Dynamo.Wpf.ViewModels.Core; -using Newtonsoft.Json; using Microsoft.Web.WebView2.Wpf; using Dynamo.Utilities; using Dynamo.Configuration; @@ -147,7 +147,7 @@ private void WebView_NavigationCompleted(object sender, Microsoft.Web.WebView2.C private void AddNotifications(List notifications) { - var notificationsList = JsonConvert.SerializeObject(notifications); + var notificationsList = JsonSerializer.Serialize(notifications); InvokeJS($"window.setNotifications({notificationsList});"); } @@ -163,7 +163,7 @@ private void RequestNotifications() using (StreamReader reader = new StreamReader(stream)) { jsonStringFile = reader.ReadToEnd(); - notificationsModel = JsonConvert.DeserializeObject(jsonStringFile); + notificationsModel = JsonSerializer.Deserialize(jsonStringFile); //We are adding a limit of months to grab the notifications var limitDate = DateTime.Now.AddMonths(-limitOfMonthsFilterNotifications); From e33d4b2baa0e19c88b5cc396c4611d87b7389db6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:13:38 +0000 Subject: [PATCH 4/7] Remove unused Newtonsoft.Json usings from chart node models Co-authored-by: QilongTang <3942418+QilongTang@users.noreply.github.com> --- src/Libraries/CoreNodeModelsWpf/Charts/BarChartNodeModel.cs | 1 - .../CoreNodeModelsWpf/Charts/BasicLineChartNodeModel.cs | 1 - src/Libraries/CoreNodeModelsWpf/Charts/HeatSeriesNodeModel.cs | 1 - src/Libraries/CoreNodeModelsWpf/Charts/PieChartNodeModel.cs | 1 - src/Libraries/CoreNodeModelsWpf/Charts/ScatterPlotNodeModel.cs | 1 - src/Libraries/CoreNodeModelsWpf/Charts/XYLineChartNodeModel.cs | 1 - 6 files changed, 6 deletions(-) diff --git a/src/Libraries/CoreNodeModelsWpf/Charts/BarChartNodeModel.cs b/src/Libraries/CoreNodeModelsWpf/Charts/BarChartNodeModel.cs index 24b6137b7ae..71c1f50df5e 100644 --- a/src/Libraries/CoreNodeModelsWpf/Charts/BarChartNodeModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/Charts/BarChartNodeModel.cs @@ -16,7 +16,6 @@ using Dynamo.Wpf; using Dynamo.Wpf.Properties; using LiveChartsCore.SkiaSharpView.Painting; -using Newtonsoft.Json; using ProtoCore.AST.AssociativeAST; using SkiaSharp.Views.WPF; diff --git a/src/Libraries/CoreNodeModelsWpf/Charts/BasicLineChartNodeModel.cs b/src/Libraries/CoreNodeModelsWpf/Charts/BasicLineChartNodeModel.cs index cbdd25dbc45..00b2b6f497e 100644 --- a/src/Libraries/CoreNodeModelsWpf/Charts/BasicLineChartNodeModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/Charts/BasicLineChartNodeModel.cs @@ -13,7 +13,6 @@ using Dynamo.Controls; using Dynamo.Graph.Nodes; using Dynamo.Wpf; -using Newtonsoft.Json; using ProtoCore.AST.AssociativeAST; using DynamoServices; using Dynamo.Wpf.Properties; diff --git a/src/Libraries/CoreNodeModelsWpf/Charts/HeatSeriesNodeModel.cs b/src/Libraries/CoreNodeModelsWpf/Charts/HeatSeriesNodeModel.cs index 821f54a676f..97c7540e98c 100644 --- a/src/Libraries/CoreNodeModelsWpf/Charts/HeatSeriesNodeModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/Charts/HeatSeriesNodeModel.cs @@ -14,7 +14,6 @@ using Dynamo.Controls; using Dynamo.Graph.Nodes; using Dynamo.Wpf; -using Newtonsoft.Json; using ProtoCore.AST.AssociativeAST; using DynamoServices; using Dynamo.Wpf.Properties; diff --git a/src/Libraries/CoreNodeModelsWpf/Charts/PieChartNodeModel.cs b/src/Libraries/CoreNodeModelsWpf/Charts/PieChartNodeModel.cs index 5d364ab51c4..93172b3820e 100644 --- a/src/Libraries/CoreNodeModelsWpf/Charts/PieChartNodeModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/Charts/PieChartNodeModel.cs @@ -14,7 +14,6 @@ using Dynamo.Controls; using Dynamo.Graph.Nodes; using Dynamo.Wpf; -using Newtonsoft.Json; using ProtoCore.AST.AssociativeAST; using DynamoServices; using Dynamo.Wpf.Properties; diff --git a/src/Libraries/CoreNodeModelsWpf/Charts/ScatterPlotNodeModel.cs b/src/Libraries/CoreNodeModelsWpf/Charts/ScatterPlotNodeModel.cs index 483c20d0371..d3b2a6615ed 100644 --- a/src/Libraries/CoreNodeModelsWpf/Charts/ScatterPlotNodeModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/Charts/ScatterPlotNodeModel.cs @@ -14,7 +14,6 @@ using Dynamo.Controls; using Dynamo.Graph.Nodes; using Dynamo.Wpf; -using Newtonsoft.Json; using ProtoCore.AST.AssociativeAST; using DynamoServices; using Dynamo.Wpf.Properties; diff --git a/src/Libraries/CoreNodeModelsWpf/Charts/XYLineChartNodeModel.cs b/src/Libraries/CoreNodeModelsWpf/Charts/XYLineChartNodeModel.cs index fcb8fc8bf54..8e1d0392751 100644 --- a/src/Libraries/CoreNodeModelsWpf/Charts/XYLineChartNodeModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/Charts/XYLineChartNodeModel.cs @@ -16,7 +16,6 @@ using Dynamo.Graph.Nodes; using Dynamo.ViewModels; using Dynamo.Wpf; -using Newtonsoft.Json; using ProtoCore.AST.AssociativeAST; using DynamoServices; using Dynamo.Wpf.Properties; From bee2b54f654069ada74e06b5c5ecf78285137f71 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:15:47 +0000 Subject: [PATCH 5/7] Address code review feedback: Add JsonPropertyName to NotificationsModel, fix security and documentation issues Co-authored-by: QilongTang <3942418+QilongTang@users.noreply.github.com> --- .../Serialization/JsonSerializationHelper.cs | 13 +++++++++++-- src/DynamoCoreWpf/UI/GuidedTour/Step.cs | 2 +- src/Notifications/NotificationsModel.cs | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/DynamoCore/Serialization/JsonSerializationHelper.cs b/src/DynamoCore/Serialization/JsonSerializationHelper.cs index 8beca39de11..b88c486c030 100644 --- a/src/DynamoCore/Serialization/JsonSerializationHelper.cs +++ b/src/DynamoCore/Serialization/JsonSerializationHelper.cs @@ -26,8 +26,8 @@ public static JsonSerializerOptions CreateSerializerOptions(params JsonConverter PropertyNameCaseInsensitive = false, AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip, - // Use invariant culture for consistent serialization - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + // Note: Using default encoder for security. If special characters need to be unescaped, + // evaluate security implications before changing to UnsafeRelaxedJsonEscaping. }; // Add custom converters @@ -164,8 +164,17 @@ public static T Deserialize(JsonElement element, JsonSerializerOptions option /// Parses a JSON string and returns a JsonDocument. /// The caller is responsible for disposing the returned JsonDocument. /// + /// The JSON string to parse + /// A JsonDocument representing the parsed JSON + /// Thrown when the JSON is malformed + /// Thrown when json parameter is null or empty public static JsonDocument ParseJson(string json) { + if (string.IsNullOrEmpty(json)) + { + throw new ArgumentException("JSON string cannot be null or empty", nameof(json)); + } + return JsonDocument.Parse(json); } diff --git a/src/DynamoCoreWpf/UI/GuidedTour/Step.cs b/src/DynamoCoreWpf/UI/GuidedTour/Step.cs index 01fbbd1c16a..ed182ab1be1 100644 --- a/src/DynamoCoreWpf/UI/GuidedTour/Step.cs +++ b/src/DynamoCoreWpf/UI/GuidedTour/Step.cs @@ -121,7 +121,7 @@ public enum StepTypes{ SURVEY, TOOLTIP, WELCOME, EXIT_TOUR}; public bool ShowLibrary { get; set; } /// - /// This propertu will hold information about the exit guide modal + /// This property will hold information about the exit guide modal /// [JsonPropertyName("ExitGuide")] internal ExitGuide ExitGuide { get; set; } diff --git a/src/Notifications/NotificationsModel.cs b/src/Notifications/NotificationsModel.cs index f13f80a394f..62491807e62 100644 --- a/src/Notifications/NotificationsModel.cs +++ b/src/Notifications/NotificationsModel.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Runtime.Serialization; using System.Text; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace Dynamo.Notifications @@ -11,12 +12,15 @@ namespace Dynamo.Notifications internal class NotificationsModel { [DataMember(Name = "version")] + [JsonPropertyName("version")] internal string Version { get; set; } [DataMember(Name = "last_update_timestamp")] + [JsonPropertyName("last_update_timestamp")] internal DateTime LastUpdate { get; set; } [DataMember(Name = "notifications")] + [JsonPropertyName("notifications")] internal List Notifications { get; set; } } @@ -24,45 +28,59 @@ internal class NotificationsModel internal class NotificationItemModel { [DataMember(Name = "id")] + [JsonPropertyName("id")] internal string Id { get; set; } [DataMember(Name = "title")] + [JsonPropertyName("title")] internal string Title { get; set; } [DataMember(Name = "link")] + [JsonPropertyName("link")] internal string Link { get; set; } [DataMember(Name = "linkTitle")] + [JsonPropertyName("linkTitle")] internal string LinkTitle { get; set; } [DataMember(Name = "longDescription")] + [JsonPropertyName("longDescription")] internal string LongDescription { get; set; } [DataMember(Name = "created")] + [JsonPropertyName("created")] internal DateTime Created { get; set; } [DataMember(Name = "thumbnail")] + [JsonPropertyName("thumbnail")] internal string Thumbnail { get; set; } [DataMember(Name = "priority")] + [JsonPropertyName("priority")] internal string Priority { get; set; } [DataMember(Name = "type")] + [JsonPropertyName("type")] internal string Type { get; set; } [DataMember(Name = "scope")] + [JsonPropertyName("scope")] internal string Scope { get; set; } [DataMember(Name = "source")] + [JsonPropertyName("source")] internal string Source { get; set; } [DataMember(Name = "status")] + [JsonPropertyName("status")] internal string Status { get; set; } [DataMember(Name = "isPinned")] + [JsonPropertyName("isPinned")] internal bool IsPinned { get; set; } [DataMember(Name = "isRead")] + [JsonPropertyName("isRead")] internal bool IsRead { get; set; } = true; } } From 6c0b470d11d3bf60798104b43b89fffed65fac5e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 22:54:17 +0000 Subject: [PATCH 6/7] Restore Newtonsoft.Json using statements in chart files that use JsonIgnore attribute Co-authored-by: QilongTang <3942418+QilongTang@users.noreply.github.com> --- src/Libraries/CoreNodeModelsWpf/Charts/BarChartNodeModel.cs | 1 + .../CoreNodeModelsWpf/Charts/BasicLineChartNodeModel.cs | 1 + src/Libraries/CoreNodeModelsWpf/Charts/HeatSeriesNodeModel.cs | 1 + src/Libraries/CoreNodeModelsWpf/Charts/PieChartNodeModel.cs | 1 + src/Libraries/CoreNodeModelsWpf/Charts/ScatterPlotNodeModel.cs | 1 + src/Libraries/CoreNodeModelsWpf/Charts/XYLineChartNodeModel.cs | 1 + 6 files changed, 6 insertions(+) diff --git a/src/Libraries/CoreNodeModelsWpf/Charts/BarChartNodeModel.cs b/src/Libraries/CoreNodeModelsWpf/Charts/BarChartNodeModel.cs index 71c1f50df5e..24b6137b7ae 100644 --- a/src/Libraries/CoreNodeModelsWpf/Charts/BarChartNodeModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/Charts/BarChartNodeModel.cs @@ -16,6 +16,7 @@ using Dynamo.Wpf; using Dynamo.Wpf.Properties; using LiveChartsCore.SkiaSharpView.Painting; +using Newtonsoft.Json; using ProtoCore.AST.AssociativeAST; using SkiaSharp.Views.WPF; diff --git a/src/Libraries/CoreNodeModelsWpf/Charts/BasicLineChartNodeModel.cs b/src/Libraries/CoreNodeModelsWpf/Charts/BasicLineChartNodeModel.cs index 00b2b6f497e..cbdd25dbc45 100644 --- a/src/Libraries/CoreNodeModelsWpf/Charts/BasicLineChartNodeModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/Charts/BasicLineChartNodeModel.cs @@ -13,6 +13,7 @@ using Dynamo.Controls; using Dynamo.Graph.Nodes; using Dynamo.Wpf; +using Newtonsoft.Json; using ProtoCore.AST.AssociativeAST; using DynamoServices; using Dynamo.Wpf.Properties; diff --git a/src/Libraries/CoreNodeModelsWpf/Charts/HeatSeriesNodeModel.cs b/src/Libraries/CoreNodeModelsWpf/Charts/HeatSeriesNodeModel.cs index 97c7540e98c..3b19b08b0aa 100644 --- a/src/Libraries/CoreNodeModelsWpf/Charts/HeatSeriesNodeModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/Charts/HeatSeriesNodeModel.cs @@ -18,6 +18,7 @@ using DynamoServices; using Dynamo.Wpf.Properties; using Dynamo.Graph.Connectors; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Dynamo.ViewModels; diff --git a/src/Libraries/CoreNodeModelsWpf/Charts/PieChartNodeModel.cs b/src/Libraries/CoreNodeModelsWpf/Charts/PieChartNodeModel.cs index 93172b3820e..e403ce009df 100644 --- a/src/Libraries/CoreNodeModelsWpf/Charts/PieChartNodeModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/Charts/PieChartNodeModel.cs @@ -19,6 +19,7 @@ using Dynamo.Wpf.Properties; using Xceed.Wpf.Toolkit; using Dynamo.Graph.Connectors; +using Newtonsoft.Json; using Dynamo.ViewModels; namespace CoreNodeModelsWpf.Charts diff --git a/src/Libraries/CoreNodeModelsWpf/Charts/ScatterPlotNodeModel.cs b/src/Libraries/CoreNodeModelsWpf/Charts/ScatterPlotNodeModel.cs index d3b2a6615ed..689e7c12f90 100644 --- a/src/Libraries/CoreNodeModelsWpf/Charts/ScatterPlotNodeModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/Charts/ScatterPlotNodeModel.cs @@ -19,6 +19,7 @@ using Dynamo.Wpf.Properties; using ProtoCore.DesignScriptParser; using Dynamo.Graph.Connectors; +using Newtonsoft.Json; using Dynamo.ViewModels; namespace CoreNodeModelsWpf.Charts diff --git a/src/Libraries/CoreNodeModelsWpf/Charts/XYLineChartNodeModel.cs b/src/Libraries/CoreNodeModelsWpf/Charts/XYLineChartNodeModel.cs index 8e1d0392751..cc64daf8901 100644 --- a/src/Libraries/CoreNodeModelsWpf/Charts/XYLineChartNodeModel.cs +++ b/src/Libraries/CoreNodeModelsWpf/Charts/XYLineChartNodeModel.cs @@ -19,6 +19,7 @@ using ProtoCore.AST.AssociativeAST; using DynamoServices; using Dynamo.Wpf.Properties; +using Newtonsoft.Json; namespace CoreNodeModelsWpf.Charts { From 66b1305a269b86eb17d1233bf426fa8227723030 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:46:30 +0000 Subject: [PATCH 7/7] Fix deserialization issues: add null checks and case-insensitive options for JSON parsing Co-authored-by: zeusongit <32665108+zeusongit@users.noreply.github.com> --- .../Serialization/JsonSerializationHelper.cs | 3 +++ src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs | 10 +++++++++- src/DynamoUtilities/DynamoFeatureFlagsManager.cs | 8 ++++++++ src/Notifications/NotificationCenterController.cs | 10 +++++++++- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/DynamoCore/Serialization/JsonSerializationHelper.cs b/src/DynamoCore/Serialization/JsonSerializationHelper.cs index b88c486c030..61d23cb6e6d 100644 --- a/src/DynamoCore/Serialization/JsonSerializationHelper.cs +++ b/src/DynamoCore/Serialization/JsonSerializationHelper.cs @@ -154,6 +154,9 @@ public static JsonElement[] GetArrayOrEmpty(JsonElement element, string property /// /// Deserializes a JsonElement to a specific type using the provided options. /// + /// The JsonElement to deserialize + /// Optional serializer options + /// The deserialized object, which may be null for reference types public static T Deserialize(JsonElement element, JsonSerializerOptions options = null) { var json = element.GetRawText(); diff --git a/src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs b/src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs index dbcfab1a4f6..a37d72905ec 100644 --- a/src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs +++ b/src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs @@ -304,8 +304,16 @@ private static List ReadGuides(string jsonFile) jsonString = r.ReadToEnd(); } + // Use case-insensitive deserialization to match JSON property names + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip + }; + //Deserialize all the information read from the json file - return JsonSerializer.Deserialize>(jsonString); + return JsonSerializer.Deserialize>(jsonString, options); } /// diff --git a/src/DynamoUtilities/DynamoFeatureFlagsManager.cs b/src/DynamoUtilities/DynamoFeatureFlagsManager.cs index 9a00c7eef26..6fe3938e5d1 100644 --- a/src/DynamoUtilities/DynamoFeatureFlagsManager.cs +++ b/src/DynamoUtilities/DynamoFeatureFlagsManager.cs @@ -71,6 +71,14 @@ internal void CacheAllFlags() // System.Text.Json deserializes numbers as JsonElement by default for Dictionary // We need to convert them to the appropriate types var rawCache = JsonSerializer.Deserialize>(dataFromCLI); + + if (rawCache == null) + { + // No flags could be deserialized; keep cache empty and return. + RaiseMessageLogged("Failed to deserialize feature flags - received null result"); + return; + } + AllFlagsCache = new Dictionary(); foreach (var kvp in rawCache) diff --git a/src/Notifications/NotificationCenterController.cs b/src/Notifications/NotificationCenterController.cs index 4aa6393d8ce..87ba0c7a08b 100755 --- a/src/Notifications/NotificationCenterController.cs +++ b/src/Notifications/NotificationCenterController.cs @@ -163,7 +163,15 @@ private void RequestNotifications() using (StreamReader reader = new StreamReader(stream)) { jsonStringFile = reader.ReadToEnd(); - notificationsModel = JsonSerializer.Deserialize(jsonStringFile); + + // Use case-insensitive deserialization to match JSON property names + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true + }; + + notificationsModel = JsonSerializer.Deserialize(jsonStringFile, options); //We are adding a limit of months to grab the notifications var limitDate = DateTime.Now.AddMonths(-limitOfMonthsFilterNotifications);