Skip to content

Commit 2ff04cd

Browse files
Treat empty string as null during deserialization with configs (#3111)
1 parent 0758a92 commit 2ff04cd

File tree

21 files changed

+164
-20
lines changed

21 files changed

+164
-20
lines changed

src/AutoRest.CSharp/Common/AutoRest/Communication/StandaloneGeneratorRunner.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,13 @@ internal static string SaveConfiguration()
176176
Utf8JsonWriterExtensions.WriteNonEmptyArray(writer, Configuration.Options.ModelFactoryForHlc, Configuration.ModelFactoryForHlc);
177177
WriteIfNotDefault(writer, Configuration.Options.UnreferencedTypesHandling, Configuration.UnreferencedTypesHandling);
178178
WriteIfNotDefault(writer, Configuration.Options.ProjectFolder, Configuration.RelativeProjectFolder);
179-
Utf8JsonWriterExtensions.WriteNonEmptyArray(writer, nameof(Configuration.ProtocolMethodList), Configuration.ProtocolMethodList);
180-
Utf8JsonWriterExtensions.WriteNonEmptyArray(writer, nameof(Configuration.SuppressAbstractBaseClasses), Configuration.SuppressAbstractBaseClasses);
179+
Utf8JsonWriterExtensions.WriteNonEmptyArray(writer, nameof(Configuration.Options.ProtocolMethodList), Configuration.ProtocolMethodList);
180+
Utf8JsonWriterExtensions.WriteNonEmptyArray(writer, nameof(Configuration.Options.SuppressAbstractBaseClasses), Configuration.SuppressAbstractBaseClasses);
181+
Utf8JsonWriterExtensions.WriteNonEmptyArray(writer, nameof(Configuration.Options.ModelsToTreatEmptyStringAsNull), Configuration.ModelsToTreatEmptyStringAsNull.ToList<string>());
182+
if (Configuration.ModelsToTreatEmptyStringAsNull.Any())
183+
{
184+
Utf8JsonWriterExtensions.WriteNonEmptyArray(writer, nameof(Configuration.IntrinsicTypesToTreatEmptyStringAsNull), Configuration.IntrinsicTypesToTreatEmptyStringAsNull.ToList<string>());
185+
}
181186

182187
Configuration.MgmtConfiguration.SaveConfiguration(writer);
183188

@@ -239,6 +244,10 @@ internal static void LoadConfiguration(string? projectPath, string outputPath, s
239244
var protocolMethods = Configuration.DeserializeArray(protocolMethodList);
240245
root.TryGetProperty(nameof(Configuration.Options.SuppressAbstractBaseClasses), out var suppressAbstractBaseClassesElement);
241246
var suppressAbstractBaseClasses = Configuration.DeserializeArray(suppressAbstractBaseClassesElement);
247+
root.TryGetProperty(nameof(Configuration.Options.ModelsToTreatEmptyStringAsNull), out var modelsToTreatEmptyStringAsNullElement);
248+
var modelsToTreatEmptyStringAsNull = Configuration.DeserializeArray(modelsToTreatEmptyStringAsNullElement);
249+
root.TryGetProperty(nameof(Configuration.IntrinsicTypesToTreatEmptyStringAsNull), out var intrinsicTypesToTreatEmptyStringAsNullElement);
250+
var intrinsicTypesToTreatEmptyStringAsNull = Configuration.DeserializeArray(intrinsicTypesToTreatEmptyStringAsNullElement);
242251
root.TryGetProperty(nameof(Configuration.Options.ModelFactoryForHlc), out var oldModelFactoryEntriesElement);
243252
var oldModelFactoryEntries = Configuration.DeserializeArray(oldModelFactoryEntriesElement);
244253

@@ -263,6 +272,8 @@ internal static void LoadConfiguration(string? projectPath, string outputPath, s
263272
projectPath ?? ReadStringOption(root, Configuration.Options.ProjectFolder),
264273
protocolMethods,
265274
suppressAbstractBaseClasses,
275+
modelsToTreatEmptyStringAsNull,
276+
intrinsicTypesToTreatEmptyStringAsNull,
266277
MgmtConfiguration.LoadConfiguration(root),
267278
MgmtTestConfiguration.LoadConfiguration(root)
268279
);

src/AutoRest.CSharp/Common/AutoRest/Plugins/Configuration.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Linq;
88
using System.Text.Json;
99
using AutoRest.CSharp.AutoRest.Communication;
10+
using Azure.Core;
1011

1112
namespace AutoRest.CSharp.Input
1213
{
@@ -37,6 +38,8 @@ public static class Options
3738
public const string UnreferencedTypesHandling = "unreferenced-types-handling";
3839
public const string ModelFactoryForHlc = "model-factory-for-hlc";
3940
public const string GenerateModelFactory = "generate-model-factory";
41+
public const string ModelsToTreatEmptyStringAsNull = "models-to-treat-empty-string-as-null";
42+
public const string AdditionalIntrinsicTypesToTreatEmptyStringAsNull = "additional-intrinsic-types-to-treat-empty-string-as-null";
4043
}
4144

4245
public enum UnreferencedTypesHandlingOption
@@ -67,6 +70,8 @@ public static void Initialize(
6770
string? projectFolder,
6871
IReadOnlyList<string> protocolMethodList,
6972
IReadOnlyList<string> suppressAbstractBaseClasses,
73+
IReadOnlyList<string> modelsToTreatEmptyStringAsNull,
74+
IReadOnlyList<string> additionalIntrinsicTypesToTreatEmptyStringAsNull,
7075
MgmtConfiguration mgmtConfiguration,
7176
MgmtTestConfiguration? mgmtTestConfiguration)
7277
{
@@ -123,6 +128,8 @@ public static void Initialize(
123128
_mgmtConfiguration = mgmtConfiguration;
124129
MgmtTestConfiguration = mgmtTestConfiguration;
125130
_suppressAbstractBaseClasses = suppressAbstractBaseClasses;
131+
_modelsToTreatEmptyStringAsNull = new HashSet<string>(modelsToTreatEmptyStringAsNull);
132+
_intrinsicTypesToTreatEmptyStringAsNull.UnionWith(additionalIntrinsicTypesToTreatEmptyStringAsNull);
126133
}
127134

128135
private static string? _outputFolder;
@@ -152,6 +159,12 @@ public static void Initialize(
152159
private static IReadOnlyList<string>? _protocolMethodList;
153160
public static IReadOnlyList<string> ProtocolMethodList => _protocolMethodList ?? throw new InvalidOperationException("Configuration has not been initialized");
154161

162+
private static HashSet<string>? _modelsToTreatEmptyStringAsNull;
163+
public static HashSet<string> ModelsToTreatEmptyStringAsNull => _modelsToTreatEmptyStringAsNull ?? throw new InvalidOperationException("Configuration has not been initialized");
164+
165+
private static HashSet<string> _intrinsicTypesToTreatEmptyStringAsNull = new HashSet<string>() { nameof(Uri), nameof(Guid), nameof(ResourceIdentifier), nameof(DateTimeOffset) };
166+
public static HashSet<string> IntrinsicTypesToTreatEmptyStringAsNull => _intrinsicTypesToTreatEmptyStringAsNull;
167+
155168
private static MgmtConfiguration? _mgmtConfiguration;
156169
public static MgmtConfiguration MgmtConfiguration => _mgmtConfiguration ?? throw new InvalidOperationException("Configuration has not been initialized");
157170

@@ -185,6 +198,8 @@ public static void Initialize(IPluginCommunication autoRest)
185198
projectFolder: autoRest.GetValue<string?>(Options.ProjectFolder).GetAwaiter().GetResult(),
186199
protocolMethodList: autoRest.GetValue<string[]?>(Options.ProtocolMethodList).GetAwaiter().GetResult() ?? Array.Empty<string>(),
187200
suppressAbstractBaseClasses: autoRest.GetValue<string[]?>(Options.SuppressAbstractBaseClasses).GetAwaiter().GetResult() ?? Array.Empty<string>(),
201+
modelsToTreatEmptyStringAsNull: autoRest.GetValue<string[]?>(Options.ModelsToTreatEmptyStringAsNull).GetAwaiter().GetResult() ?? Array.Empty<string>(),
202+
additionalIntrinsicTypesToTreatEmptyStringAsNull: autoRest.GetValue<string[]?>(Options.AdditionalIntrinsicTypesToTreatEmptyStringAsNull).GetAwaiter().GetResult() ?? Array.Empty<string>(),
188203
mgmtConfiguration: MgmtConfiguration.GetConfiguration(autoRest),
189204
mgmtTestConfiguration: MgmtTestConfiguration.GetConfiguration(autoRest)
190205
);

src/AutoRest.CSharp/Common/Generation/Writers/JsonCodeWriterExtensions.cs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ private static void ToSerializeCall(this CodeWriter writer, FormattableString wr
306306
}
307307
}
308308

309-
private static void DeserializeIntoObjectProperties(this CodeWriter writer, IEnumerable<JsonPropertySerialization> propertySerializations, FormattableString itemVariable, Dictionary<JsonPropertySerialization, ObjectPropertyVariable> propertyVariables)
309+
private static void DeserializeIntoObjectProperties(this CodeWriter writer, IEnumerable<JsonPropertySerialization> propertySerializations, FormattableString itemVariable, Dictionary<JsonPropertySerialization, ObjectPropertyVariable> propertyVariables, bool shouldTreatEmptyStringAsNull)
310310
{
311311
foreach (JsonPropertySerialization property in propertySerializations.Where(p => !p.ShouldSkipDeserialization))
312312
{
@@ -315,7 +315,8 @@ private static void DeserializeIntoObjectProperties(this CodeWriter writer, IEnu
315315
{
316316
if (property.ValueType?.IsNullable == true)
317317
{
318-
using (writer.Scope($"if ({itemVariable}.Value.ValueKind == {typeof(JsonValueKind)}.Null)"))
318+
var emptyStringCheck = GetEmptyStringCheckClause(property, itemVariable, shouldTreatEmptyStringAsNull);
319+
using (writer.Scope($"if ({itemVariable}.Value.ValueKind == {typeof(JsonValueKind)}.Null{emptyStringCheck})"))
319320
{
320321
writer.Line($"{propertyVariables[property].Declaration} = null;");
321322
writer.Append($"continue;");
@@ -326,17 +327,18 @@ private static void DeserializeIntoObjectProperties(this CodeWriter writer, IEnu
326327
property.ValueType?.Equals(typeof(string)) !=
327328
true) //https://github.com/Azure/autorest.csharp/issues/922
328329
{
330+
var emptyStringCheck = GetEmptyStringCheckClause(property, itemVariable, shouldTreatEmptyStringAsNull);
329331
if (Configuration.AzureArm && property.ValueType?.Equals(typeof(Uri)) == true)
330332
{
331-
using (writer.Scope($"if ({itemVariable}.Value.ValueKind == {typeof(JsonValueKind)}.Null)"))
333+
using (writer.Scope($"if ({itemVariable}.Value.ValueKind == {typeof(JsonValueKind)}.Null{emptyStringCheck})"))
332334
{
333335
writer.Line($"{propertyVariables[property].Declaration} = null;");
334336
writer.Append($"continue;");
335337
}
336338
}
337339
else
338340
{
339-
using (writer.Scope($"if ({itemVariable}.Value.ValueKind == {typeof(JsonValueKind)}.Null)"))
341+
using (writer.Scope($"if ({itemVariable}.Value.ValueKind == {typeof(JsonValueKind)}.Null{emptyStringCheck})"))
340342
{
341343
writer.UseNamespace(typeof(JsonElementExtensions).Namespace!);
342344
writer.Line($"{itemVariable}.{nameof(JsonElementExtensions.ThrowNonNullablePropertyIsNull)}();");
@@ -357,7 +359,7 @@ private static void DeserializeIntoObjectProperties(this CodeWriter writer, IEnu
357359
var nestedItemVariable = new CodeWriterDeclaration("property");
358360
using (writer.Scope($"foreach (var {nestedItemVariable:D} in {itemVariable:I}.Value.EnumerateObject())"))
359361
{
360-
writer.DeserializeIntoObjectProperties(property.PropertySerializations, $"{nestedItemVariable:I}", propertyVariables);
362+
writer.DeserializeIntoObjectProperties(property.PropertySerializations, $"{nestedItemVariable:I}", propertyVariables, shouldTreatEmptyStringAsNull);
361363
}
362364
}
363365
else
@@ -370,6 +372,22 @@ private static void DeserializeIntoObjectProperties(this CodeWriter writer, IEnu
370372
}
371373
}
372374

375+
private static FormattableString GetEmptyStringCheckClause(JsonPropertySerialization property, FormattableString itemVariable, bool shouldTreatEmptyStringAsNull)
376+
{
377+
FormattableString result = $"";
378+
if (!shouldTreatEmptyStringAsNull)
379+
return result;
380+
if (property.ValueSerialization is JsonValueSerialization valueSerialization
381+
&& valueSerialization.Type.IsFrameworkType)
382+
{
383+
if (Configuration.IntrinsicTypesToTreatEmptyStringAsNull.Contains(valueSerialization.Type.FrameworkType.Name))
384+
{
385+
result = $" || {itemVariable}.Value.ValueKind == {typeof(JsonValueKind)}.{nameof(JsonValueKind.String)} && {itemVariable}.Value.GetString().Length == 0";
386+
}
387+
}
388+
return result;
389+
}
390+
373391
private static FormattableString GetOptionalFormattable(JsonPropertySerialization target, ObjectPropertyVariable variable)
374392
{
375393
var targetType = target.PropertyType;
@@ -559,7 +577,7 @@ serialization.Discriminator.Value is not null &&
559577
var itemVariable = new CodeWriterDeclaration("property");
560578
using (writer.Scope($"foreach (var {itemVariable:D} in element.EnumerateObject())"))
561579
{
562-
writer.DeserializeIntoObjectProperties(serialization.Properties, $"{itemVariable:I}", propertyVariables);
580+
writer.DeserializeIntoObjectProperties(serialization.Properties, $"{itemVariable:I}", propertyVariables, Configuration.ModelsToTreatEmptyStringAsNull.Contains(serialization.Type.Name));
563581

564582
if (objAdditionalProperties?.ValueSerialization != null)
565583
{

src/CADL.Extension/Emitter.Csharp/src/emitter.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,28 @@ export async function $onEmit(context: EmitContext<NetEmitterOptions>) {
166166
SingleTopLevelClient: options["single-top-level-client"],
167167
"unreferenced-types-handling":
168168
options["unreferenced-types-handling"],
169-
"model-namespace": options["model-namespace"]
169+
"model-namespace": options["model-namespace"],
170+
ModelsToTreatEmptyStringAsNull:
171+
options["models-to-treat-empty-string-as-null"],
172+
IntrinsicTypesToTreatEmptyStringAsNull: options[
173+
"models-to-treat-empty-string-as-null"
174+
]
175+
? options[
176+
"additional-intrinsic-types-to-treat-empty-string-as-null"
177+
].concat(
178+
[
179+
"Uri",
180+
"Guid",
181+
"ResourceIdentifier",
182+
"DateTimeOffset"
183+
].filter(
184+
(item) =>
185+
options[
186+
"additional-intrinsic-types-to-treat-empty-string-as-null"
187+
].indexOf(item) < 0
188+
)
189+
)
190+
: undefined
170191
} as Configuration;
171192

172193
await program.host.writeFile(

src/CADL.Extension/Emitter.Csharp/src/options.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export type NetEmitterOptions = {
1919
"save-inputs"?: boolean;
2020
"model-namespace"?: boolean;
2121
debug?: boolean;
22+
"models-to-treat-empty-string-as-null"?: string[];
23+
"additional-intrinsic-types-to-treat-empty-string-as-null"?: string[];
2224
} & DpgEmitterOptions;
2325

2426
export const NetEmitterOptionsSchema: JSONSchemaType<NetEmitterOptions> = {
@@ -48,7 +50,17 @@ export const NetEmitterOptionsSchema: JSONSchemaType<NetEmitterOptions> = {
4850
"generate-protocol-methods": { type: "boolean", nullable: true },
4951
"generate-convenience-methods": { type: "boolean", nullable: true },
5052
"package-name": { type: "string", nullable: true },
51-
debug: { type: "boolean", nullable: true }
53+
debug: { type: "boolean", nullable: true },
54+
"models-to-treat-empty-string-as-null": {
55+
type: "array",
56+
nullable: true,
57+
items: { type: "string" }
58+
},
59+
"additional-intrinsic-types-to-treat-empty-string-as-null": {
60+
type: "array",
61+
nullable: true,
62+
items: { type: "string" }
63+
}
5264
},
5365
required: []
5466
};
@@ -64,7 +76,9 @@ const defaultOptions = {
6476
"generate-protocol-methods": true,
6577
"generate-convenience-methods": true,
6678
"package-name": undefined,
67-
debug: undefined
79+
debug: undefined,
80+
"models-to-treat-empty-string-as-null": undefined,
81+
"additional-intrinsic-types-to-treat-empty-string-as-null": []
6882
};
6983

7084
export function resolveOptions(context: EmitContext<NetEmitterOptions>) {

src/CADL.Extension/Emitter.Csharp/src/type/Configuration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ export interface Configuration {
1212
| "internalize"
1313
| "keepAll";
1414
"model-namespace"?: boolean;
15+
"models-to-treat-empty-string-as-null"?: string[];
16+
"additional-intrinsic-types-to-treat-empty-string-as-null"?: string[];
1517
}

test/AutoRest.TestServer.Tests/Mgmt/Unit/MgmtRestOperationTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ public void Setup()
7070
projectFolder: "/..",
7171
protocolMethodList: Array.Empty<string>(),
7272
suppressAbstractBaseClasses: Array.Empty<string>(),
73+
modelsToTreatEmptyStringAsNull: Array.Empty<string>(),
74+
additionalIntrinsicTypesToTreatEmptyStringAsNull: Array.Empty<string>(),
7375
mgmtConfiguration: mgmtConfiguration,
7476
mgmtTestConfiguration: null);
7577
}

test/AutoRest.TestServerLowLevel.Tests/LowLevel/Generation/ModelGenerationTestBase.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ public void init()
5555
projectFolder: ".",
5656
protocolMethodList: Array.Empty<string>(),
5757
suppressAbstractBaseClasses: Array.Empty<string>(),
58+
modelsToTreatEmptyStringAsNull: Array.Empty<string>(),
59+
additionalIntrinsicTypesToTreatEmptyStringAsNull: Array.Empty<string>(),
5860
mgmtConfiguration: null,
5961
mgmtTestConfiguration: null);
6062
}

test/CadlRanchProjects/models/property-optional/Generated/Configuration.json

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/CadlRanchProjects/models/property-optional/Generated/Models/DatetimeProperty.Serialization.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ internal static DatetimeProperty DeserializeDatetimeProperty(JsonElement element
4343
{
4444
if (property0.NameEquals("property"u8))
4545
{
46-
if (property0.Value.ValueKind == JsonValueKind.Null)
46+
if (property0.Value.ValueKind == JsonValueKind.Null || property0.Value.ValueKind == JsonValueKind.String && property0.Value.GetString().Length == 0)
4747
{
4848
property = null;
4949
continue;

0 commit comments

Comments
 (0)