Skip to content

Commit 38ab3c7

Browse files
committed
feat: Implement collection count optimization for collection deserialization, including a new [GenJsonSkipCountOptimization] attribute and updated documentation.
1 parent 00a328b commit 38ab3c7

4 files changed

Lines changed: 91 additions & 8 deletions

File tree

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,29 @@ public partial class Cat : Animal
289289

290290
**Deserialization**: `Animal.FromJson(...)` will inspect the `$type` property and deserialize into the correct derived type (`Dog` or `Cat`). If the type is unknown or missing (for abstract bases), it returns `null`.
291291

292+
### 11. Collection Count Optimization
293+
294+
GenJson optimizes collection deserialization (Lists, Arrays, Dictionaries) by pre-allocating the collection with the exact size. This avoids resizing overhead during population.
295+
296+
**How it works:**
297+
- **Serialization**: The generator automatically emits a hidden property named after the collection with a `$` prefix (e.g., `"$MyList": 5`) immediately before the collection property.
298+
- **Deserialization**: The parser reads this count property first and initializes the collection with the correct capacity (e.g., `new List<int>(5)`).
299+
300+
> [!NOTE]
301+
> GenJson can still parse standard JSON without the count property. If the property is missing, it will automatically fall back to standard resizing behavior.
302+
303+
**Disabling Optimization:**
304+
If you need strictly standard JSON or cannot support the extra property, you can disable this optimization using the `[GenJsonSkipCountOptimization]` attribute on your class or struct.
305+
306+
```csharp
307+
[GenJson]
308+
[GenJsonSkipCountOptimization] // Disables usage of $MyList property
309+
public partial class MyClass
310+
{
311+
public List<int> MyList { get; set; }
312+
}
313+
```
314+
292315
## How It Works
293316

294317
GenJson analyzes your code during compilation and generates specialized serialization code.

src/GenJson.Generator/GenJsonSourceGenerator.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public record ClassData(
8585
bool HasGenJsonBase,
8686
bool IsNullableContext,
8787
string? PolymorphicDiscriminatorProp,
88+
bool SkipCountOptimization,
8889
EquatableList<DerivedTypeData> DerivedTypes);
8990

9091
public record DerivedTypeData(string TypeName, string DiscriminatorValue, bool IsIntDiscriminator);
@@ -312,7 +313,9 @@ private static bool IsSyntaxNodeValid(SyntaxNode node, CancellationToken ct)
312313
}
313314
}
314315

315-
return new ClassData(typeSymbol.Name, typeNameSpace, new EquatableList<PropertyData>(constructorArgs), new EquatableList<PropertyData>(initProperties), new EquatableList<PropertyData>(properties), keyword, typeSymbol.IsAbstract, hasGenJsonBase, isNullableContext, polymorphicDiscriminatorProp, new EquatableList<DerivedTypeData>(derivedTypes));
316+
bool skipCountOptimization = typeSymbol.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "GenJson.GenJsonSkipCountOptimizationAttribute");
317+
318+
return new ClassData(typeSymbol.Name, typeNameSpace, new EquatableList<PropertyData>(constructorArgs), new EquatableList<PropertyData>(initProperties), new EquatableList<PropertyData>(properties), keyword, typeSymbol.IsAbstract, hasGenJsonBase, isNullableContext, polymorphicDiscriminatorProp, skipCountOptimization, new EquatableList<DerivedTypeData>(derivedTypes));
316319
}
317320

318321
private static PropertyData? GetPropertyForParameter(
@@ -659,7 +662,7 @@ private void Generate(SourceProductionContext context, ClassData data)
659662

660663
sb.Append(indent);
661664
sb.AppendLine("propertyCount++;");
662-
if (prop.Type is GenJsonDataType.Enumerable en)
665+
if (!data.SkipCountOptimization && prop.Type is GenJsonDataType.Enumerable en)
663666
{
664667
if (en.IsArray)
665668
{
@@ -704,7 +707,7 @@ private void Generate(SourceProductionContext context, ClassData data)
704707
sb.AppendLine("}");
705708
}
706709
}
707-
else if (prop.Type is GenJsonDataType.Dictionary)
710+
else if (!data.SkipCountOptimization && prop.Type is GenJsonDataType.Dictionary)
708711
{
709712
sb.Append(indent);
710713
sb.Append("size += ");
@@ -877,7 +880,7 @@ private void Generate(SourceProductionContext context, ClassData data)
877880
else stateSpan = 1;
878881
}
879882

880-
if (prop.Type is GenJsonDataType.Enumerable en)
883+
if (!data.SkipCountOptimization && prop.Type is GenJsonDataType.Enumerable en)
881884
{
882885
if (en.IsArray)
883886
{
@@ -938,7 +941,7 @@ private void Generate(SourceProductionContext context, ClassData data)
938941
sb.AppendLine("}");
939942
}
940943
}
941-
else if (prop.Type is GenJsonDataType.Dictionary)
944+
else if (!data.SkipCountOptimization && prop.Type is GenJsonDataType.Dictionary)
942945
{
943946
sb.Append(indent);
944947
sb.Append("global::GenJson.GenJsonWriter.WriteString(span, ref index, \"$");
@@ -1078,7 +1081,7 @@ private void Generate(SourceProductionContext context, ClassData data)
10781081
sb.Append(prop.Name);
10791082
sb.AppendLine(" = default;");
10801083

1081-
if (prop.Type is GenJsonDataType.Enumerable or GenJsonDataType.Dictionary)
1084+
if (!data.SkipCountOptimization && (prop.Type is GenJsonDataType.Enumerable or GenJsonDataType.Dictionary))
10821085
{
10831086
sb.Append(" int _");
10841087
sb.Append(prop.Name);
@@ -1162,7 +1165,7 @@ private void Generate(SourceProductionContext context, ClassData data)
11621165
sb.AppendLine(" bool matched = false;");
11631166
foreach (var prop in allProperties)
11641167
{
1165-
if (prop.Type is GenJsonDataType.Enumerable or GenJsonDataType.Dictionary)
1168+
if (!data.SkipCountOptimization && (prop.Type is GenJsonDataType.Enumerable or GenJsonDataType.Dictionary))
11661169
{
11671170
sb.Append(" if (global::GenJson.GenJsonParser.MatchesKey(json, ref index, \"$");
11681171
sb.Append(prop.JsonName);
@@ -1182,7 +1185,7 @@ private void Generate(SourceProductionContext context, ClassData data)
11821185
sb.AppendLine("\"))");
11831186
sb.AppendLine(" {");
11841187
sb.AppendLine(" if (!global::GenJson.GenJsonParser.TryExpect(json, ref index, ':')) return null;");
1185-
GenerateParseValue(sb, prop.Type, "_" + prop.Name, " ", 0, (prop.Type is GenJsonDataType.Enumerable or GenJsonDataType.Dictionary) ? $"_{prop.Name}_count" : null);
1188+
GenerateParseValue(sb, prop.Type, "_" + prop.Name, " ", 0, (!data.SkipCountOptimization && (prop.Type is GenJsonDataType.Enumerable or GenJsonDataType.Dictionary)) ? $"_{prop.Name}_count" : null);
11861189
if (data.IsNullableContext && prop.IsValueType && !prop.IsNullable)
11871190
{
11881191
sb.Append(" _");
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System.Collections.Generic;
2+
using NUnit.Framework;
3+
4+
namespace GenJson.Tests;
5+
6+
[GenJson]
7+
[GenJsonSkipCountOptimization]
8+
public partial record SkipOptimizationClass(List<int> List, int[] Array, Dictionary<string, int> Dictionary);
9+
10+
public class TestSkipOptimization
11+
{
12+
[Test]
13+
public void TestSerialization_SkipsCountProperty()
14+
{
15+
var obj = new SkipOptimizationClass([1, 2], [3, 4], new() { { "5", 6 } });
16+
var json = obj.ToJson();
17+
18+
// Expect NO count properties
19+
var expected = """{"List":[1,2],"Array":[3,4],"Dictionary":{"5":6}}""";
20+
Assert.That(json, Is.EqualTo(expected));
21+
}
22+
23+
[Test]
24+
public void TestDeserialization_WorksWithoutCountProperty()
25+
{
26+
var json = """{"List":[1,2],"Array":[3,4],"Dictionary":{"5":6}}""";
27+
var obj = SkipOptimizationClass.FromJson(json)!;
28+
29+
Assert.That(obj, Is.Not.Null);
30+
Assert.That(obj.List, Is.EqualTo(new List<int> { 1, 2 }));
31+
Assert.That(obj.Array, Is.EqualTo(new int[] { 3, 4 }));
32+
Assert.That(obj.Dictionary, Is.EqualTo(new Dictionary<string, int> { { "5", 6 } }));
33+
}
34+
35+
[Test]
36+
public void TestDeserialization_IgnoresCountPropertyIfPresent()
37+
{
38+
// Even if count property is present in JSON (e.g. from older version or other system),
39+
// it should be treated as unknown property and ignored since we are skipping optimization.
40+
var json = """{"$List":2,"List":[1,2],"$Array":2,"Array":[3,4],"$Dictionary":1,"Dictionary":{"5":6}}""";
41+
var obj = SkipOptimizationClass.FromJson(json)!;
42+
43+
Assert.That(obj, Is.Not.Null);
44+
Assert.That(obj.List, Is.EqualTo(new List<int> { 1, 2 }));
45+
Assert.That(obj.Array, Is.EqualTo(new int[] { 3, 4 }));
46+
Assert.That(obj.Dictionary, Is.EqualTo(new Dictionary<string, int> { { "5", 6 } }));
47+
}
48+
}

src/GenJson/GenJsonAttribute.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,13 @@ public GenJsonDerivedTypeAttribute(Type type, object? typeDiscriminatorValue = n
115115
public Type Type { get; }
116116
public object? TypeDiscriminatorValue { get; }
117117
}
118+
/// <summary>
119+
/// This attribute can be used to skip the count optimization for collections.
120+
/// When applied to a class or struct, the generator will not emit the $PropertyName count property
121+
/// and will not look for it during deserialization.
122+
/// </summary>
123+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
124+
public sealed class GenJsonSkipCountOptimizationAttribute : Attribute
125+
{
126+
}
118127
}

0 commit comments

Comments
 (0)