Skip to content

Commit a835ac8

Browse files
committed
Json : Normalize
1 parent 3f88b97 commit a835ac8

File tree

13 files changed

+466
-33
lines changed

13 files changed

+466
-33
lines changed

src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NewtonsoftJsonParsingConfig.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,20 @@ public class NewtonsoftJsonParsingConfig : ParsingConfig
1515
/// <summary>
1616
/// The default <see cref="DynamicJsonClassOptions"/> to use.
1717
/// </summary>
18-
public DynamicJsonClassOptions? DynamicJsonClassOptions { get; set; }
18+
public DynamicJsonClassOptions? DynamicJsonClassOptions { get; set; }
19+
20+
/// <summary>
21+
/// Gets or sets a value indicating whether the objecs in an array should be normalized before processing.
22+
/// </summary>
23+
public bool Normalize { get; set; } = true;
24+
25+
/// <summary>
26+
/// Gets or sets the behavior to apply when a property value does not exist during normalization.
27+
/// </summary>
28+
/// <remarks>
29+
/// Use this property to control how the normalization process handles properties that are missing or undefined.
30+
/// The selected behavior may affect the output or error handling of normalization operations.
31+
/// The default value is <see cref="NormalizationNonExistingPropertyValueBehavior.UseDefaultValue"/>.
32+
/// </remarks>
33+
public NormalizationNonExistingPropertyValueBehavior NormalizationNonExistingPropertyValueBehavior { get; set; }
1934
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace System.Linq.Dynamic.Core.NewtonsoftJson.Config;
2+
3+
/// <summary>
4+
/// Specifies the behavior to use when setting a property vlue that does not exist or is missing during normalization.
5+
/// </summary>
6+
public enum NormalizationNonExistingPropertyValueBehavior
7+
{
8+
/// <summary>
9+
/// Specifies that the default value should be used.
10+
/// </summary>
11+
UseDefaultValue = 0,
12+
13+
/// <summary>
14+
/// Specifies that null values should be used.
15+
/// </summary>
16+
UseNull = 1
17+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Newtonsoft.Json.Linq;
2+
3+
namespace ConsoleApp3;
4+
5+
internal struct JsonValueInfo(JTokenType type, object? value)
6+
{
7+
public JTokenType Type { get; } = type;
8+
9+
public object? Value { get; } = value;
10+
}

src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections;
22
using System.Linq.Dynamic.Core.NewtonsoftJson.Config;
33
using System.Linq.Dynamic.Core.NewtonsoftJson.Extensions;
4+
using System.Linq.Dynamic.Core.NewtonsoftJson.Utils;
45
using System.Linq.Dynamic.Core.Validation;
56
using JetBrains.Annotations;
67
using Newtonsoft.Json.Linq;
@@ -870,7 +871,13 @@ private static JArray ToJArray(Func<IQueryable> func)
870871

871872
private static IQueryable ToQueryable(JArray source, NewtonsoftJsonParsingConfig? config = null)
872873
{
873-
return source.ToDynamicJsonClassArray(config?.DynamicJsonClassOptions).AsQueryable();
874+
var normalized = config?.Normalize == true ?
875+
NormalizeUtils.NormalizeArray(source, config.NormalizationNonExistingPropertyValueBehavior):
876+
source;
877+
878+
return normalized
879+
.ToDynamicJsonClassArray(config?.DynamicJsonClassOptions)
880+
.AsQueryable();
874881
}
875882
#endregion
876883
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using System.Collections.Generic;
2+
using System.Linq.Dynamic.Core.NewtonsoftJson.Config;
3+
using ConsoleApp3;
4+
using Newtonsoft.Json.Linq;
5+
6+
namespace System.Linq.Dynamic.Core.NewtonsoftJson.Utils;
7+
8+
internal static class NormalizeUtils
9+
{
10+
/// <summary>
11+
/// Normalizes an array of JSON objects so that each object contains all properties found in the array,
12+
/// including nested objects. Missing properties will have null values.
13+
/// </summary>
14+
internal static JArray NormalizeArray(JArray jsonArray, NormalizationNonExistingPropertyValueBehavior normalizationBehavior)
15+
{
16+
if (jsonArray.Count(item => item is not JObject) > 0)
17+
{
18+
return jsonArray;
19+
}
20+
21+
var schema = BuildSchema(jsonArray);
22+
var normalizedArray = new JArray();
23+
24+
foreach (var jo in jsonArray.OfType<JObject>())
25+
{
26+
var normalizedObj = NormalizeObject(jo, schema, normalizationBehavior);
27+
normalizedArray.Add(normalizedObj);
28+
}
29+
30+
return normalizedArray;
31+
}
32+
33+
private static Dictionary<string, JsonValueInfo> BuildSchema(JArray array)
34+
{
35+
var schema = new Dictionary<string, JsonValueInfo>();
36+
37+
foreach (var item in array)
38+
{
39+
if (item is JObject obj)
40+
{
41+
MergeSchema(schema, obj);
42+
}
43+
}
44+
45+
return schema;
46+
}
47+
48+
private static void MergeSchema(Dictionary<string, JsonValueInfo> schema, JObject obj)
49+
{
50+
foreach (var prop in obj.Properties())
51+
{
52+
if (prop.Value is JObject nested)
53+
{
54+
if (!schema.ContainsKey(prop.Name))
55+
{
56+
schema[prop.Name] = new JsonValueInfo(JTokenType.Object, new Dictionary<string, JsonValueInfo>());
57+
}
58+
59+
MergeSchema((Dictionary<string, JsonValueInfo>)schema[prop.Name].Value!, nested);
60+
}
61+
else
62+
{
63+
if (!schema.ContainsKey(prop.Name))
64+
{
65+
schema[prop.Name] = new JsonValueInfo(prop.Value.Type, null);
66+
}
67+
}
68+
}
69+
}
70+
71+
private static JObject NormalizeObject(JObject source, Dictionary<string, JsonValueInfo> schema, NormalizationNonExistingPropertyValueBehavior normalizationBehavior)
72+
{
73+
var result = new JObject();
74+
75+
foreach (var key in schema.Keys)
76+
{
77+
if (schema[key].Value is Dictionary<string, JsonValueInfo> nestedSchema)
78+
{
79+
result[key] = source.ContainsKey(key) && source[key] is JObject jo ? NormalizeObject(jo, nestedSchema, normalizationBehavior) : CreateEmptyObject(nestedSchema, normalizationBehavior);
80+
}
81+
else
82+
{
83+
if (source.ContainsKey(key))
84+
{
85+
result[key] = source[key];
86+
}
87+
else
88+
{
89+
result[key] = normalizationBehavior == NormalizationNonExistingPropertyValueBehavior.UseDefaultValue ? GetDefaultValue(schema[key]) : JValue.CreateNull();
90+
}
91+
}
92+
}
93+
94+
return result;
95+
}
96+
97+
private static JObject CreateEmptyObject(Dictionary<string, JsonValueInfo> schema, NormalizationNonExistingPropertyValueBehavior normalizationBehavior)
98+
{
99+
var obj = new JObject();
100+
foreach (var key in schema.Keys)
101+
{
102+
if (schema[key].Value is Dictionary<string, JsonValueInfo> nestedSchema)
103+
{
104+
obj[key] = CreateEmptyObject(nestedSchema, normalizationBehavior);
105+
}
106+
else
107+
{
108+
obj[key] = normalizationBehavior == NormalizationNonExistingPropertyValueBehavior.UseDefaultValue ? GetDefaultValue(schema[key]) : JValue.CreateNull();
109+
}
110+
}
111+
112+
return obj;
113+
}
114+
115+
private static JToken GetDefaultValue(JsonValueInfo jType)
116+
{
117+
return jType.Type switch
118+
{
119+
JTokenType.Array => new JArray(),
120+
JTokenType.Boolean => default(bool),
121+
JTokenType.Bytes => new byte[0],
122+
JTokenType.Date => DateTime.MinValue,
123+
JTokenType.Float => default(float),
124+
JTokenType.Guid => Guid.Empty,
125+
JTokenType.Integer => default(int),
126+
JTokenType.String => string.Empty,
127+
JTokenType.TimeSpan => TimeSpan.MinValue,
128+
_ => JValue.CreateNull(),
129+
};
130+
}
131+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace System.Linq.Dynamic.Core.SystemTextJson.Config;
2+
3+
/// <summary>
4+
/// Specifies the behavior to use when setting a property vlue that does not exist or is missing during normalization.
5+
/// </summary>
6+
public enum NormalizationNonExistingPropertyValueBehavior
7+
{
8+
/// <summary>
9+
/// Specifies that the default value should be used.
10+
/// </summary>
11+
UseDefaultValue = 0,
12+
13+
/// <summary>
14+
/// Specifies that null values should be used.
15+
/// </summary>
16+
UseNull = 1
17+
}

src/System.Linq.Dynamic.Core.SystemTextJson/Config/SystemTextJsonParsingConfig.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,23 @@ public class SystemTextJsonParsingConfig : ParsingConfig
88
/// <summary>
99
/// The default ParsingConfig for <see cref="SystemTextJsonParsingConfig"/>.
1010
/// </summary>
11-
public new static SystemTextJsonParsingConfig Default { get; } = new();
11+
public new static SystemTextJsonParsingConfig Default { get; } = new SystemTextJsonParsingConfig
12+
{
13+
ConvertObjectToSupportComparison = true
14+
};
15+
16+
/// <summary>
17+
/// Gets or sets a value indicating whether the objecs in an array should be normalized before processing.
18+
/// </summary>
19+
public bool Normalize { get; set; } = true;
20+
21+
/// <summary>
22+
/// Gets or sets the behavior to apply when a property value does not exist during normalization.
23+
/// </summary>
24+
/// <remarks>
25+
/// Use this property to control how the normalization process handles properties that are missing or undefined.
26+
/// The selected behavior may affect the output or error handling of normalization operations.
27+
/// The default value is <see cref="NormalizationNonExistingPropertyValueBehavior.UseDefaultValue"/>.
28+
/// </remarks>
29+
public NormalizationNonExistingPropertyValueBehavior NormalizationNonExistingPropertyValueBehavior { get; set; }
1230
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#if !NET8_0_OR_GREATER
2+
namespace System.Text.Json.Nodes;
3+
4+
internal static class JsonValueExtensions
5+
{
6+
internal static JsonValueKind? GetValueKind(this JsonNode node)
7+
{
8+
if (node is JsonObject)
9+
{
10+
return JsonValueKind.Object;
11+
}
12+
13+
if (node is JsonArray)
14+
{
15+
return JsonValueKind.Array;
16+
}
17+
18+
return node.GetValue<object>() is JsonElement je ? je.ValueKind : null;
19+
}
20+
}
21+
#endif
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using System.Text.Json;
2+
3+
internal struct JsonValueInfo(JsonValueKind type, object? value)
4+
{
5+
public JsonValueKind Type { get; } = type;
6+
7+
public object? Value { get; } = value;
8+
}

src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq.Dynamic.Core.SystemTextJson.Utils;
66
using System.Linq.Dynamic.Core.Validation;
77
using System.Text.Json;
8+
using ConsoleApp3;
89
using JetBrains.Annotations;
910

1011
namespace System.Linq.Dynamic.Core.SystemTextJson;
@@ -1093,13 +1094,20 @@ private static JsonDocument ToJsonDocumentArray(Func<IQueryable> func)
10931094
// ReSharper disable once UnusedParameter.Local
10941095
private static IQueryable ToQueryable(JsonDocument source, SystemTextJsonParsingConfig? config = null)
10951096
{
1097+
config = config ?? SystemTextJsonParsingConfig.Default;
1098+
config.ConvertObjectToSupportComparison = true;
1099+
10961100
var array = source.RootElement;
10971101
if (array.ValueKind != JsonValueKind.Array)
10981102
{
10991103
throw new NotSupportedException("The source is not a JSON array.");
11001104
}
11011105

1102-
return JsonDocumentExtensions.ToDynamicJsonClassArray(array).AsQueryable();
1106+
var normalized = config.Normalize ?
1107+
NormalizeUtils.NormalizeJsonDocument(source, config.NormalizationNonExistingPropertyValueBehavior) :
1108+
source;
1109+
1110+
return JsonDocumentExtensions.ToDynamicJsonClassArray(normalized.RootElement).AsQueryable();
11031111
}
11041112
#endregion
11051113
}

0 commit comments

Comments
 (0)