Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions src/DynamoCore/Serialization/JsonSerializationHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Dynamo.Serialization
{
/// <summary>
/// Helper class for JSON serialization using System.Text.Json.
/// Provides utilities to replace Newtonsoft.Json functionality.
/// </summary>
public static class JsonSerializationHelper
{
/// <summary>
/// Creates default JsonSerializerOptions for Dynamo serialization.
/// </summary>
/// <param name="converters">Optional custom converters to include</param>
/// <returns>Configured JsonSerializerOptions</returns>
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,
// Note: Using default encoder for security. If special characters need to be unescaped,
// evaluate security implications before changing to UnsafeRelaxedJsonEscaping.
};

// Add custom converters
if (converters != null)
{
foreach (var converter in converters)
{
options.Converters.Add(converter);
}
}

return options;
}

/// <summary>
/// Creates JsonSerializerOptions for deserialization with backward compatibility.
/// </summary>
/// <param name="converters">Optional custom converters to include</param>
/// <returns>Configured JsonSerializerOptions</returns>
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;
}

/// <summary>
/// Safely gets a string value from a JsonElement.
/// </summary>
public static string GetStringOrDefault(JsonElement element, string defaultValue = "")
{
return element.ValueKind == JsonValueKind.String ? element.GetString() ?? defaultValue : defaultValue;
}

/// <summary>
/// Safely gets an int value from a JsonElement.
/// </summary>
public static int GetInt32OrDefault(JsonElement element, int defaultValue = 0)
{
return element.ValueKind == JsonValueKind.Number && element.TryGetInt32(out var value) ? value : defaultValue;
}

/// <summary>
/// Safely gets a double value from a JsonElement.
/// </summary>
public static double GetDoubleOrDefault(JsonElement element, double defaultValue = 0.0)
{
return element.ValueKind == JsonValueKind.Number && element.TryGetDouble(out var value) ? value : defaultValue;
}

/// <summary>
/// Safely gets a bool value from a JsonElement.
/// </summary>
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;
}

/// <summary>
/// Safely gets a Guid value from a JsonElement.
/// </summary>
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;
}

/// <summary>
/// Tries to get a property from a JsonElement.
/// </summary>
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;
}

/// <summary>
/// Gets an array of JsonElements from a property, or empty array if not found.
/// </summary>
public static JsonElement[] GetArrayOrEmpty(JsonElement element, string propertyName)
{
if (TryGetProperty(element, propertyName, out var property) && property.ValueKind == JsonValueKind.Array)
{
var list = new List<JsonElement>();
foreach (var item in property.EnumerateArray())
{
list.Add(item);
}
return list.ToArray();
}
return Array.Empty<JsonElement>();
}

/// <summary>
/// Deserializes a JsonElement to a specific type using the provided options.
/// </summary>
/// <param name="element">The JsonElement to deserialize</param>
/// <param name="options">Optional serializer options</param>
/// <returns>The deserialized object, which may be null for reference types</returns>
public static T Deserialize<T>(JsonElement element, JsonSerializerOptions options = null)
{
var json = element.GetRawText();
return JsonSerializer.Deserialize<T>(json, options);
}
Comment on lines +160 to +164
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Deserialize<T> method does not handle the case where JsonSerializer.Deserialize<T> returns null for reference types. This could lead to unexpected null returns. Consider documenting this behavior or adding validation.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 66b1305. Updated documentation to clarify that Deserialize may return null for reference types when deserialization fails.


/// <summary>
/// Parses a JSON string and returns a JsonDocument.
/// The caller is responsible for disposing the returned JsonDocument.
/// </summary>
/// <param name="json">The JSON string to parse</param>
/// <returns>A JsonDocument representing the parsed JSON</returns>
/// <exception cref="JsonException">Thrown when the JSON is malformed</exception>
/// <exception cref="ArgumentException">Thrown when json parameter is null or empty</exception>
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);
}

/// <summary>
/// Writes a JSON value with error handling.
/// </summary>
public static void WriteValue(Utf8JsonWriter writer, string propertyName, string value)
{
writer.WriteString(propertyName, value);
}

/// <summary>
/// Writes a JSON value with error handling.
/// </summary>
public static void WriteValue(Utf8JsonWriter writer, string propertyName, int value)
{
writer.WriteNumber(propertyName, value);
}

/// <summary>
/// Writes a JSON value with error handling.
/// </summary>
public static void WriteValue(Utf8JsonWriter writer, string propertyName, double value)
{
writer.WriteNumber(propertyName, value);
}

/// <summary>
/// Writes a JSON value with error handling.
/// </summary>
public static void WriteValue(Utf8JsonWriter writer, string propertyName, bool value)
{
writer.WriteBoolean(propertyName, value);
}

/// <summary>
/// Writes a JSON value with error handling.
/// </summary>
public static void WriteValue(Utf8JsonWriter writer, string propertyName, Guid value)
{
writer.WriteString(propertyName, value.ToString());
}
}
}
14 changes: 7 additions & 7 deletions src/DynamoCoreWpf/UI/GuidedTour/CutOffArea.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Windows;
using Dynamo.Core;
using Newtonsoft.Json;
using System.Text.Json.Serialization;

namespace Dynamo.Wpf.UI.GuidedTour
{
Expand All @@ -20,37 +20,37 @@ public class CutOffArea : NotificationObject
/// <summary>
/// Since the box that cuts the elements has its size fixed, this variable applies a value to fix its Width
/// </summary>
[JsonProperty(nameof(WidthBoxDelta))]
[JsonPropertyName(nameof(WidthBoxDelta))]
public double WidthBoxDelta { get => widthBoxDelta; set => widthBoxDelta = value; }

/// <summary>
/// Since the box that cuts the elements has its size fixed, this variable applies a value to fix its Height
/// </summary>
[JsonProperty(nameof(HeightBoxDelta))]
[JsonPropertyName(nameof(HeightBoxDelta))]
public double HeightBoxDelta { get => heightBoxDelta; set => heightBoxDelta = value; }

/// <summary>
/// This property will move the CutOff area horizontally over the X axis
/// </summary>
[JsonProperty(nameof(XPosOffset))]
[JsonPropertyName(nameof(XPosOffset))]
public double XPosOffset { get => xPosOffset; set => xPosOffset = value; }

/// <summary>
/// This property will move the CutOff area vertically over the Y axis
/// </summary>
[JsonProperty(nameof(YPosOffset))]
[JsonPropertyName(nameof(YPosOffset))]
public double YPosOffset { get => yPosOffset; set => yPosOffset = value; }

/// <summary>
/// In the case the cutoff area is not the same than HostControlInfo.HostUIElementString the this property needs to be populated
/// </summary>
[JsonProperty(nameof(WindowElementNameString))]
[JsonPropertyName(nameof(WindowElementNameString))]
public string WindowElementNameString { get; set; }

/// <summary>
/// In cases when we need to put the CutOff area over a node in the Workspace this property will be used
/// </summary>
[JsonProperty(nameof(NodeId))]
[JsonPropertyName(nameof(NodeId))]
public string NodeId { get; set; }

/// <summary>
Expand Down
12 changes: 6 additions & 6 deletions src/DynamoCoreWpf/UI/GuidedTour/ExitGuide.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,25 +12,25 @@ internal class ExitGuide
/// <summary>
/// Represents the Height of Exit Guide modal
/// </summary>
[JsonProperty("Height")]
[JsonPropertyName("Height")]
public double Height { get; set; }

/// <summary>
/// Represents the Width of Exit Guide modal
/// </summary>
[JsonProperty("Width")]
[JsonPropertyName("Width")]
public double Width { get; set; }

/// <summary>
/// Represents the key to the resources related to the Title of Exit Guide modal
/// </summary>
[JsonProperty("Title")]
[JsonPropertyName("Title")]
public string Title { get; set; }

/// <summary>
/// Represents the formatted text key to the resources related to the Title of Exit Guide modal
/// </summary>
[JsonProperty("FormattedText")]
[JsonPropertyName("FormattedText")]
public string FormattedText { get; set; }
}
}
10 changes: 5 additions & 5 deletions src/DynamoCoreWpf/UI/GuidedTour/Guide.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,7 +10,6 @@
using Dynamo.ViewModels;
using Dynamo.Wpf.Properties;
using Dynamo.Wpf.Views.GuidedTour;
using Newtonsoft.Json;

namespace Dynamo.Wpf.UI.GuidedTour
{
Expand All @@ -21,13 +21,13 @@ public class Guide
/// <summary>
/// This list will contain all the steps per guide read from a json file
/// </summary>
[JsonProperty("GuideSteps")]
[JsonPropertyName("GuideSteps")]
internal List<Step> GuideSteps { get; set; }

/// <summary>
/// This property represent the name of the Guide, e.g. "Get Started", "Packages"
/// </summary>
[JsonProperty("Name")]
[JsonPropertyName("Name")]
internal string Name { get; set; }

/// <summary>
Expand All @@ -36,13 +36,13 @@ public class Guide
/// 1 - User interface guide
/// 2 - Onboarding guide
/// </summary>
[JsonProperty("SequenceOrder")]
[JsonPropertyName("SequenceOrder")]
internal int SequenceOrder { get; set; }

/// <summary>
/// This property has the resource key string for the guide
/// </summary>
[JsonProperty("GuideNameResource")]
[JsonPropertyName("GuideNameResource")]
internal string GuideNameResource { get; set; }


Expand Down
12 changes: 10 additions & 2 deletions src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -304,8 +304,16 @@ private static List<Guide> 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 JsonConvert.DeserializeObject<List<Guide>>(jsonString);
return JsonSerializer.Deserialize<List<Guide>>(jsonString, options);
}

/// <summary>
Expand Down
Loading
Loading