Skip to content

Commit fc7e74d

Browse files
committed
fix: fall back from huge generated json contexts
1 parent 4252a4a commit fc7e74d

File tree

5 files changed

+333
-41
lines changed

5 files changed

+333
-41
lines changed

src/libs/AutoSDK.CLI/Commands/GenerateCommand.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -327,15 +327,16 @@ private async Task HandleAsync(ParseResult parseResult)
327327
throw new NotSupportedException($"Unsupported language '{language}'. Currently only 'csharp' is supported.");
328328
}
329329

330-
var coreResult = CorePipeline.Prepare(
331-
((yaml, settings), GlobalSettings: settings),
332-
static (document, currentSettings) => document.GetSchemas((CSharpSettings)currentSettings),
333-
CSharpPipeline.ApplyModelNaming,
334-
static text => text.ToClassName(),
335-
static text => text.ToPropertyName());
336-
var plugin = CSharpLanguagePlugin.Instance;
337-
var data = plugin.Enrich(coreResult);
338-
var files = plugin
330+
var data = CSharpPipeline.PrepareAndEnrich(
331+
((yaml, settings), GlobalSettings: settings));
332+
333+
if (settings.GenerateJsonSerializerContextTypes &&
334+
string.IsNullOrWhiteSpace(data.Converters.Settings.JsonSerializerContext))
335+
{
336+
Console.WriteLine("Warning: Disabled generated System.Text.Json source-generation context because some union-heavy types exceeded compiler metadata limits.");
337+
}
338+
339+
var files = CSharpLanguagePlugin.Instance
339340
.GenerateFiles(data)
340341
.Where(x => !x.IsEmpty)
341342
.ToArray();

src/libs/AutoSDK.CSharp/Pipeline/CSharpPipeline.cs

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ public static Models.Data PrepareAndEnrich(
1515
CancellationToken cancellationToken = default)
1616
{
1717
var totalTime = System.Diagnostics.Stopwatch.StartNew();
18+
var data = PrepareAndEnrichCore(tuple, allowJsonSerializerContextFallback: true, cancellationToken);
19+
return data with
20+
{
21+
Times = data.Times with
22+
{
23+
Total = totalTime.Elapsed,
24+
},
25+
};
26+
}
27+
28+
private static Models.Data PrepareAndEnrichCore(
29+
((string Text, Settings Settings) Context, Settings GlobalSettings) tuple,
30+
bool allowJsonSerializerContextFallback,
31+
CancellationToken cancellationToken)
32+
{
1833
var coreResult = CorePipeline.Prepare(
1934
tuple,
2035
static (document, settings) => document.GetSchemas((CSharpSettings)settings),
@@ -24,13 +39,37 @@ public static Models.Data PrepareAndEnrich(
2439
cancellationToken);
2540

2641
var data = Enrich(coreResult, cancellationToken);
27-
return data with
42+
43+
if (allowJsonSerializerContextFallback &&
44+
ShouldDisableGeneratedJsonSerializerContext(tuple.Context.Settings, data))
2845
{
29-
Times = data.Times with
46+
var fallbackSettings = tuple.Context.Settings with
3047
{
31-
Total = totalTime.Elapsed,
32-
},
33-
};
48+
JsonSerializerContext = string.Empty,
49+
GenerateJsonSerializerContextTypes = false,
50+
};
51+
var fallbackGlobalSettings = tuple.GlobalSettings with
52+
{
53+
JsonSerializerContext = string.Empty,
54+
GenerateJsonSerializerContextTypes = false,
55+
};
56+
57+
return PrepareAndEnrichCore(
58+
((tuple.Context.Text, fallbackSettings), fallbackGlobalSettings),
59+
allowJsonSerializerContextFallback: false,
60+
cancellationToken);
61+
}
62+
63+
return data;
64+
}
65+
66+
private static bool ShouldDisableGeneratedJsonSerializerContext(
67+
Settings settings,
68+
Models.Data data)
69+
{
70+
return settings.GenerateJsonSerializerContextTypes &&
71+
settings.UsesSystemTextJson() &&
72+
Sources.HasOversizedGeneratedJsonSerializerContextTypeNames(data.Types);
3473
}
3574

3675
public static void ApplyModelNaming(IReadOnlyList<SchemaContext> schemas)

src/libs/AutoSDK.CSharp/Sources/Sources.JsonSerializerContext.cs

Lines changed: 118 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using AutoSDK.Extensions;
22
using AutoSDK.Models;
3+
using System.Globalization;
4+
using System.Security.Cryptography;
5+
using System.Text;
36
namespace AutoSDK.Generation;
47

58
public static partial class Sources
@@ -123,16 +126,10 @@ private static Dictionary<string, string> BuildExplicitTypeInfoPropertyNames(
123126
}
124127

125128
var baseName = $"{GetImplicitTypeInfoPropertyName(type)}_{SanitizeTypeInfoPropertyName(type)}";
126-
var name = baseName;
127-
var suffix = 2;
128-
129-
while (usedNames.Contains(name))
130-
{
131-
name = $"{baseName}_{suffix++}";
132-
}
133-
134-
usedNames.Add(name);
135-
explicitNames[type] = name;
129+
explicitNames[type] = ReserveExplicitTypeInfoPropertyName(
130+
usedNames,
131+
baseName,
132+
type);
136133
}
137134
}
138135

@@ -160,16 +157,10 @@ private static Dictionary<string, string> BuildExplicitTypeInfoPropertyNames(
160157

161158
var implicitName = GetImplicitTypeInfoPropertyName(type);
162159
var baseName = $"{implicitName}2";
163-
var name = baseName;
164-
var suffix = 3;
165-
166-
while (usedNames.Contains(name))
167-
{
168-
name = $"{implicitName}{suffix++}";
169-
}
170-
171-
usedNames.Add(name);
172-
explicitNames[type] = name;
160+
explicitNames[type] = ReserveExplicitTypeInfoPropertyName(
161+
usedNames,
162+
baseName,
163+
type);
173164
}
174165

175166
// Phase 3: Handle collisions between explicit types and STJ's implicit nullable naming.
@@ -196,19 +187,119 @@ private static Dictionary<string, string> BuildExplicitTypeInfoPropertyNames(
196187
}
197188

198189
var baseName = $"{implicitName}2";
199-
var name = baseName;
200-
var suffix = 3;
190+
explicitNames[type] = ReserveExplicitTypeInfoPropertyName(
191+
usedNames,
192+
baseName,
193+
type);
194+
}
195+
196+
return explicitNames;
197+
}
198+
199+
private const int MaxExplicitTypeInfoPropertyNameLength = 120;
200+
private const int MaxGeneratedTypeInfoNameLength = 120;
201+
202+
public static bool HasOversizedGeneratedJsonSerializerContextTypeNames(
203+
EquatableArray<TypeData> types)
204+
{
205+
var distinctTypeNames = types
206+
.Select(static x => x.CSharpTypeWithoutNullability)
207+
.Where(static x => !string.IsNullOrWhiteSpace(x))
208+
.Distinct(StringComparer.Ordinal)
209+
.ToArray();
210+
211+
if (distinctTypeNames.Any(static x => GetImplicitTypeInfoPropertyName(x).Length > MaxGeneratedTypeInfoNameLength))
212+
{
213+
return true;
214+
}
215+
216+
if (types.Any(static x =>
217+
x.IsValueType &&
218+
x.CSharpTypeWithNullability != x.CSharpTypeWithoutNullability &&
219+
$"Nullable{GetImplicitTypeInfoPropertyName(x.CSharpTypeWithoutNullability)}".Length > MaxGeneratedTypeInfoNameLength))
220+
{
221+
return true;
222+
}
223+
224+
var concreteListTypes = GetConcreteListTypes(
225+
types
226+
.Select(static x => x.CSharpTypeWithNullability)
227+
.Where(static x => !string.IsNullOrWhiteSpace(x))
228+
.Distinct(StringComparer.Ordinal)
229+
.ToArray());
230+
231+
return concreteListTypes.Any(static x => GetImplicitTypeInfoPropertyName(x).Length > MaxGeneratedTypeInfoNameLength);
232+
}
201233

202-
while (usedNames.Contains(name))
234+
private static string ReserveExplicitTypeInfoPropertyName(
235+
HashSet<string> usedNames,
236+
string baseName,
237+
string type)
238+
{
239+
for (var suffix = 0; ; suffix++)
240+
{
241+
var candidateSeed = suffix == 0
242+
? baseName
243+
: $"{baseName}_{suffix + 2}";
244+
var candidate = NormalizeExplicitTypeInfoPropertyName(candidateSeed, type);
245+
246+
if (usedNames.Add(candidate))
203247
{
204-
name = $"{implicitName}{suffix++}";
248+
return candidate;
205249
}
250+
}
251+
}
206252

207-
usedNames.Add(name);
208-
explicitNames[type] = name;
253+
private static string NormalizeExplicitTypeInfoPropertyName(string candidate, string type)
254+
{
255+
if (candidate.Length <= MaxExplicitTypeInfoPropertyNameLength)
256+
{
257+
return candidate;
209258
}
210259

211-
return explicitNames;
260+
var prefix = SanitizeTypeInfoPropertyName(GetSimpleTypeName(type));
261+
var hash = ComputeStableTypeInfoPropertyNameHash(candidate);
262+
var maxPrefixLength = MaxExplicitTypeInfoPropertyNameLength - hash.Length - 1;
263+
264+
if (maxPrefixLength <= 0)
265+
{
266+
return hash;
267+
}
268+
269+
if (prefix.Length > maxPrefixLength)
270+
{
271+
prefix = prefix.Substring(0, maxPrefixLength);
272+
}
273+
274+
if (prefix.Length == 0)
275+
{
276+
prefix = "Type";
277+
if (prefix.Length > maxPrefixLength)
278+
{
279+
prefix = prefix.Substring(0, maxPrefixLength);
280+
}
281+
}
282+
283+
return $"{prefix}_{hash}";
284+
}
285+
286+
private static string ComputeStableTypeInfoPropertyNameHash(string value)
287+
{
288+
var bytes = Encoding.UTF8.GetBytes(value);
289+
#if NET10_0_OR_GREATER
290+
var hash = SHA256.HashData(bytes);
291+
#else
292+
using var sha256 = SHA256.Create();
293+
var hash = sha256.ComputeHash(bytes);
294+
#endif
295+
var builder = new StringBuilder(capacity: 16);
296+
297+
for (var index = 0; index < 8; index++)
298+
{
299+
builder.Append(hash[index].ToString("x2", CultureInfo.InvariantCulture));
300+
}
301+
302+
return builder.ToString();
212303
}
213304

214305
private static string GetImplicitTypeInfoPropertyName(string type)

src/tests/AutoSDK.IntegrationTests.Cli/CliTests.cs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,116 @@ await GenerateFromContentAsync(
872872
targetFramework: "net10.0");
873873
}
874874

875+
[TestMethod]
876+
public async Task Generate_WithHugeUnionTypeInfoPropertyName_Builds()
877+
{
878+
var policyNames = new[]
879+
{
880+
"NewModerateModerateRequestPolicieToxicity",
881+
"NewModerateModerateRequestPoliciePersonalInformation",
882+
"NewModerateModerateRequestPolicieToxicitySevere",
883+
"NewModerateModerateRequestPolicieHate",
884+
"NewModerateModerateRequestPolicieIllicit",
885+
"NewModerateModerateRequestPolicieIllicitDrugs",
886+
"NewModerateModerateRequestPolicieIllicitAlcohol",
887+
"NewModerateModerateRequestPolicieIllicitFirearms",
888+
"NewModerateModerateRequestPolicieIllicitTobacco",
889+
"NewModerateModerateRequestPolicieIllicitGambling",
890+
"NewModerateModerateRequestPolicieCannabis",
891+
"NewModerateModerateRequestPolicieAdult",
892+
"NewModerateModerateRequestPolicieCrypto",
893+
"NewModerateModerateRequestPolicieSexual",
894+
"NewModerateModerateRequestPolicieFlirtation",
895+
"NewModerateModerateRequestPolicieProfanity",
896+
"NewModerateModerateRequestPolicieViolence",
897+
"NewModerateModerateRequestPolicieSelfHarm",
898+
"NewModerateModerateRequestPolicieSpam",
899+
"NewModerateModerateRequestPolicieSelfPromotion",
900+
"NewModerateModerateRequestPoliciePolitical",
901+
"NewModerateModerateRequestPolicieReligion",
902+
"NewModerateModerateRequestPolicieCodeAbuse",
903+
"NewModerateModerateRequestPoliciePiiMasking",
904+
"NewModerateModerateRequestPolicieUrlMasking",
905+
"NewModerateModerateRequestPolicieGuideline",
906+
};
907+
var anyOfRefs = string.Join(
908+
Environment.NewLine,
909+
policyNames.Select(name => $" - $ref: '#/components/schemas/{name}'"));
910+
var policySchemas = string.Join(
911+
Environment.NewLine + Environment.NewLine,
912+
policyNames.Select(name => $$"""
913+
{{name}}:
914+
type: object
915+
properties:
916+
enabled:
917+
type: boolean
918+
"""));
919+
var spec = $$"""
920+
openapi: 3.0.3
921+
info:
922+
title: HugeUnion
923+
version: 1.0.0
924+
paths:
925+
/moderate:
926+
post:
927+
operationId: moderate
928+
requestBody:
929+
required: true
930+
content:
931+
application/json:
932+
schema:
933+
$ref: '#/components/schemas/ModerateRequest'
934+
responses:
935+
'200':
936+
description: OK
937+
content:
938+
application/json:
939+
schema:
940+
type: object
941+
properties:
942+
ok:
943+
type: boolean
944+
components:
945+
schemas:
946+
ModerateRequest:
947+
type: object
948+
required:
949+
- content
950+
properties:
951+
content:
952+
type: string
953+
policies:
954+
type: array
955+
items:
956+
anyOf:
957+
{{anyOfRefs}}
958+
959+
{{policySchemas}}
960+
""";
961+
962+
await GenerateFromContentAsync(
963+
fileName: "huge-union-typeinfo.yaml",
964+
specContent: spec,
965+
targetFramework: "net10.0",
966+
namespaceValue: "HugeUnion",
967+
assertGeneratedOutput: async outputDirectory =>
968+
{
969+
var contextFile = Path.Combine(outputDirectory, "HugeUnion.JsonSerializerContext.g.cs");
970+
var contextTypesFile = Path.Combine(outputDirectory, "HugeUnion.JsonSerializerContextTypes.g.cs");
971+
972+
File.Exists(contextFile).Should().BeFalse();
973+
File.Exists(contextTypesFile).Should().BeFalse();
974+
975+
var generatedContents = await Task.WhenAll(
976+
Directory.EnumerateFiles(outputDirectory, "*.g.cs", SearchOption.AllDirectories)
977+
.Select(path => File.ReadAllTextAsync(path)));
978+
var content = string.Join("\n\n", generatedContents);
979+
980+
content.Should().Contain("JsonSerializerOptions");
981+
content.Should().NotContain("JsonSerializerContext { get; set; } =");
982+
});
983+
}
984+
875985
[TestMethod]
876986
public async Task Generate_WithRequiredNullableAnyOfRequestProperty_Builds()
877987
{

0 commit comments

Comments
 (0)