Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,20 @@ public class NewtonsoftJsonParsingConfig : ParsingConfig
/// <summary>
/// The default <see cref="DynamicJsonClassOptions"/> to use.
/// </summary>
public DynamicJsonClassOptions? DynamicJsonClassOptions { get; set; }
public DynamicJsonClassOptions? DynamicJsonClassOptions { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the objects in an array should be normalized before processing.
/// </summary>
public bool Normalize { get; set; } = true;

/// <summary>
/// Gets or sets the behavior to apply when a property value does not exist during normalization.
/// </summary>
/// <remarks>
/// Use this property to control how the normalization process handles properties that are missing or undefined.
/// The selected behavior may affect the output or error handling of normalization operations.
/// The default value is <see cref="NormalizationNonExistingPropertyBehavior.UseDefaultValue"/>.
/// </remarks>
public NormalizationNonExistingPropertyBehavior NormalizationNonExistingPropertyValueBehavior { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace System.Linq.Dynamic.Core.NewtonsoftJson.Config;

/// <summary>
/// Specifies the behavior to use when setting a property value that does not exist or is missing during normalization.
/// </summary>
public enum NormalizationNonExistingPropertyBehavior
{
/// <summary>
/// Specifies that the default value should be used.
/// </summary>
UseDefaultValue = 0,

/// <summary>
/// Specifies that null values should be used.
/// </summary>
UseNull = 1
}
10 changes: 10 additions & 0 deletions src/System.Linq.Dynamic.Core.NewtonsoftJson/JsonValueInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Newtonsoft.Json.Linq;

namespace System.Linq.Dynamic.Core.NewtonsoftJson;

internal readonly struct JsonValueInfo(JTokenType type, object? value)
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

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

Extra space in parameter declaration: "type, object?" should be "type, object?".

Suggested change
internal readonly struct JsonValueInfo(JTokenType type, object? value)
internal readonly struct JsonValueInfo(JTokenType type, object? value)

Copilot uses AI. Check for mistakes.
{
public JTokenType Type { get; } = type;

public object? Value { get; } = value;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections;
using System.Linq.Dynamic.Core.NewtonsoftJson.Config;
using System.Linq.Dynamic.Core.NewtonsoftJson.Extensions;
using System.Linq.Dynamic.Core.NewtonsoftJson.Utils;
using System.Linq.Dynamic.Core.Validation;
using JetBrains.Annotations;
using Newtonsoft.Json.Linq;
Expand Down Expand Up @@ -870,7 +871,13 @@ private static JArray ToJArray(Func<IQueryable> func)

private static IQueryable ToQueryable(JArray source, NewtonsoftJsonParsingConfig? config = null)
{
return source.ToDynamicJsonClassArray(config?.DynamicJsonClassOptions).AsQueryable();
var normalized = config?.Normalize == true ?
NormalizeUtils.NormalizeArray(source, config.NormalizationNonExistingPropertyValueBehavior):
source;

return normalized
.ToDynamicJsonClassArray(config?.DynamicJsonClassOptions)
.AsQueryable();
}
#endregion
}
131 changes: 131 additions & 0 deletions src/System.Linq.Dynamic.Core.NewtonsoftJson/Utils/NormalizeUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System.Collections.Generic;
using System.Linq.Dynamic.Core.NewtonsoftJson.Config;
using Newtonsoft.Json.Linq;

namespace System.Linq.Dynamic.Core.NewtonsoftJson.Utils;

internal static class NormalizeUtils
{
/// <summary>
/// Normalizes an array of JSON objects so that each object contains all properties found in the array,
/// including nested objects. Missing properties will have null values.
/// </summary>
internal static JArray NormalizeArray(JArray jsonArray, NormalizationNonExistingPropertyBehavior normalizationBehavior)
{
if (jsonArray.Any(item => item is not JObject))
{
return jsonArray;
}

var schema = BuildSchema(jsonArray);
var normalizedArray = new JArray();

foreach (var jo in jsonArray.OfType<JObject>())
{
var normalizedObj = NormalizeObject(jo, schema, normalizationBehavior);
normalizedArray.Add(normalizedObj);
}

return normalizedArray;
}

private static Dictionary<string, JsonValueInfo> BuildSchema(JArray array)
{
var schema = new Dictionary<string, JsonValueInfo>();

foreach (var item in array)
{
if (item is JObject obj)
{
MergeSchema(schema, obj);
}
Comment on lines +36 to +41
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Suggested change
foreach (var item in array)
{
if (item is JObject obj)
{
MergeSchema(schema, obj);
}
foreach (var obj in array.OfType<JObject>())
{
MergeSchema(schema, obj);

Copilot uses AI. Check for mistakes.
}

return schema;
}

private static void MergeSchema(Dictionary<string, JsonValueInfo> schema, JObject obj)
{
foreach (var prop in obj.Properties())
{
if (prop.Value is JObject nested)
{
if (!schema.TryGetValue(prop.Name, out var jsonValueInfo))
{
jsonValueInfo = new JsonValueInfo(JTokenType.Object, new Dictionary<string, JsonValueInfo>());
schema[prop.Name] = jsonValueInfo;
}

MergeSchema((Dictionary<string, JsonValueInfo>)jsonValueInfo.Value!, nested);
}
else
{
if (!schema.ContainsKey(prop.Name))
{
schema[prop.Name] = new JsonValueInfo(prop.Value.Type, null);
}
}
}
}

private static JObject NormalizeObject(JObject source, Dictionary<string, JsonValueInfo> schema, NormalizationNonExistingPropertyBehavior normalizationBehavior)
{
var result = new JObject();

foreach (var key in schema.Keys)
{
if (schema[key].Value is Dictionary<string, JsonValueInfo> nestedSchema)
{
result[key] = source.ContainsKey(key) && source[key] is JObject jo ? NormalizeObject(jo, nestedSchema, normalizationBehavior) : CreateEmptyObject(nestedSchema, normalizationBehavior);
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

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

Inefficient use of 'ContainsKey' and indexer.

Suggested change
result[key] = source.ContainsKey(key) && source[key] is JObject jo ? NormalizeObject(jo, nestedSchema, normalizationBehavior) : CreateEmptyObject(nestedSchema, normalizationBehavior);
var value = source[key];
result[key] = value is JObject jo ? NormalizeObject(jo, nestedSchema, normalizationBehavior) : CreateEmptyObject(nestedSchema, normalizationBehavior);

Copilot uses AI. Check for mistakes.
}
else
{
if (source.ContainsKey(key))
{
result[key] = source[key];
Comment on lines +83 to +85
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

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

Inefficient use of 'ContainsKey' and indexer.

Suggested change
if (source.ContainsKey(key))
{
result[key] = source[key];
if (source.TryGetValue(key, out var value))
{
result[key] = value;

Copilot uses AI. Check for mistakes.
}
else
{
result[key] = normalizationBehavior == NormalizationNonExistingPropertyBehavior.UseDefaultValue ? GetDefaultValue(schema[key]) : JValue.CreateNull();
}
}
}

return result;
}

private static JObject CreateEmptyObject(Dictionary<string, JsonValueInfo> schema, NormalizationNonExistingPropertyBehavior normalizationBehavior)
{
var obj = new JObject();
foreach (var key in schema.Keys)
{
if (schema[key].Value is Dictionary<string, JsonValueInfo> nestedSchema)
{
obj[key] = CreateEmptyObject(nestedSchema, normalizationBehavior);
}
else
{
obj[key] = normalizationBehavior == NormalizationNonExistingPropertyBehavior.UseDefaultValue ? GetDefaultValue(schema[key]) : JValue.CreateNull();
}
}

return obj;
}

private static JToken GetDefaultValue(JsonValueInfo jType)
{
return jType.Type switch
{
JTokenType.Array => new JArray(),
JTokenType.Boolean => default(bool),
JTokenType.Bytes => new byte[0],
JTokenType.Date => DateTime.MinValue,
JTokenType.Float => default(float),
JTokenType.Guid => Guid.Empty,
JTokenType.Integer => default(int),
JTokenType.String => string.Empty,
JTokenType.TimeSpan => TimeSpan.MinValue,
_ => JValue.CreateNull(),
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace System.Linq.Dynamic.Core.SystemTextJson.Config;

/// <summary>
/// Specifies the behavior to use when setting a property vlue that does not exist or is missing during normalization.
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

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

Spelling error: "vlue" should be "value".

Suggested change
/// Specifies the behavior to use when setting a property vlue that does not exist or is missing during normalization.
/// Specifies the behavior to use when setting a property value that does not exist or is missing during normalization.

Copilot uses AI. Check for mistakes.
/// </summary>
public enum NormalizationNonExistingPropertyBehavior
{
/// <summary>
/// Specifies that the default value should be used.
/// </summary>
UseDefaultValue = 0,

/// <summary>
/// Specifies that null values should be used.
/// </summary>
UseNull = 1
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,23 @@ public class SystemTextJsonParsingConfig : ParsingConfig
/// <summary>
/// The default ParsingConfig for <see cref="SystemTextJsonParsingConfig"/>.
/// </summary>
public new static SystemTextJsonParsingConfig Default { get; } = new();
public new static SystemTextJsonParsingConfig Default { get; } = new SystemTextJsonParsingConfig
{
ConvertObjectToSupportComparison = true
};

/// <summary>
/// Gets or sets a value indicating whether the objects in an array should be normalized before processing.
/// </summary>
public bool Normalize { get; set; } = true;

/// <summary>
/// Gets or sets the behavior to apply when a property value does not exist during normalization.
/// </summary>
/// <remarks>
/// Use this property to control how the normalization process handles properties that are missing or undefined.
/// The selected behavior may affect the output or error handling of normalization operations.
/// The default value is <see cref="NormalizationNonExistingPropertyBehavior.UseDefaultValue"/>.
/// </remarks>
public NormalizationNonExistingPropertyBehavior NormalizationNonExistingPropertyValueBehavior { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#if !NET8_0_OR_GREATER
namespace System.Text.Json.Nodes;

internal static class JsonValueExtensions
{
internal static JsonValueKind? GetValueKind(this JsonNode node)
{
if (node is JsonObject)
{
return JsonValueKind.Object;
}

if (node is JsonArray)
{
return JsonValueKind.Array;
}

return node.GetValue<object>() is JsonElement je ? je.ValueKind : null;
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

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

The method attempts to get a JsonElement by calling GetValue<object>() which may throw exceptions for certain JsonValue types. Consider using TryGetValue or handling potential exceptions, or checking the node type before attempting the conversion.

Suggested change
return node.GetValue<object>() is JsonElement je ? je.ValueKind : null;
if (node is JsonValue jsonValue)
{
var value = jsonValue.GetValue<object>();
if (value is JsonElement je)
{
return je.ValueKind;
}
}
return null;

Copilot uses AI. Check for mistakes.
}
}
#endif
10 changes: 10 additions & 0 deletions src/System.Linq.Dynamic.Core.SystemTextJson/JsonValueInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Text.Json;

namespace System.Linq.Dynamic.Core.SystemTextJson;

internal readonly struct JsonValueInfo(JsonValueKind type, object? value)
{
public JsonValueKind Type { get; } = type;

public object? Value { get; } = value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -1093,13 +1093,20 @@ private static JsonDocument ToJsonDocumentArray(Func<IQueryable> func)
// ReSharper disable once UnusedParameter.Local
private static IQueryable ToQueryable(JsonDocument source, SystemTextJsonParsingConfig? config = null)
{
config = config ?? SystemTextJsonParsingConfig.Default;
config.ConvertObjectToSupportComparison = true;
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

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

Configuration mutation issue: This code mutates a potentially shared config object by setting ConvertObjectToSupportComparison = true on line 1097. If the caller passes in a config object, they may not expect it to be modified. Consider creating a new config instance if a non-default config is passed, or document that the config will be modified.

Suggested change
config.ConvertObjectToSupportComparison = true;
// Avoid mutating the caller's config object
if (ReferenceEquals(config, SystemTextJsonParsingConfig.Default))
{
// Default config is safe to mutate
config.ConvertObjectToSupportComparison = true;
}
else
{
// Create a copy to avoid mutating the caller's config
config = new SystemTextJsonParsingConfig
{
Normalize = config.Normalize,
NormalizationNonExistingPropertyValueBehavior = config.NormalizationNonExistingPropertyValueBehavior,
ConvertObjectToSupportComparison = true
// Copy other properties as needed
};
}

Copilot uses AI. Check for mistakes.

var array = source.RootElement;
if (array.ValueKind != JsonValueKind.Array)
{
throw new NotSupportedException("The source is not a JSON array.");
}

return JsonDocumentExtensions.ToDynamicJsonClassArray(array).AsQueryable();
var normalized = config.Normalize ?
NormalizeUtils.NormalizeJsonDocument(source, config.NormalizationNonExistingPropertyValueBehavior) :
source;

return JsonDocumentExtensions.ToDynamicJsonClassArray(normalized.RootElement).AsQueryable();
}
#endregion
}
Loading
Loading