Skip to content

Commit 4169410

Browse files
authored
Fix serializer including legacy scope properties when modern properties present (#756)
* all green * add more tests, all green
1 parent b40ea58 commit 4169410

2 files changed

Lines changed: 155 additions & 0 deletions

File tree

src/Bicep.Types.UnitTests/TypeSerializerTests.cs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
using System.Text.Json;
99
using System.Text.Json.Nodes;
1010
using System.Text.Json.Serialization;
11+
using Azure.Bicep.Types;
1112
using Azure.Bicep.Types.Concrete;
13+
using Azure.Bicep.Types.Index;
1214
using Azure.Bicep.Types.Serialization;
1315
using FluentAssertions;
1416
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -414,6 +416,157 @@ public void ResourceType_with_legacy_unknown_scope_deserializes_as_all()
414416
deserializedResource.WritableScopes.Should().Be(ScopeType.All);
415417
}
416418

419+
[TestMethod]
420+
public void ResourceType_with_modern_scopes_should_not_serialize_null_legacy_properties()
421+
{
422+
var factory = new TypeFactory(Enumerable.Empty<TypeBase>());
423+
var stringType = factory.Create(() => new StringType());
424+
var objectType = factory.Create(() => new ObjectType(
425+
name: "request@v1",
426+
properties: new Dictionary<string, ObjectTypeProperty>
427+
{
428+
["uri"] = new(factory.GetReference(stringType), ObjectTypePropertyFlags.Required, "The HTTP request URI")
429+
},
430+
additionalProperties: null));
431+
432+
// Create ResourceType with ONLY modern properties
433+
var resourceType = factory.Create(() => new ResourceType(
434+
name: "request@v1",
435+
body: factory.GetReference(objectType),
436+
functions: null,
437+
writableScopes_in: ScopeType.All,
438+
readableScopes_in: ScopeType.All));
439+
440+
// Serialize
441+
string serializedJson;
442+
using (var stream = new MemoryStream())
443+
{
444+
TypeSerializer.Serialize(stream, factory.GetTypes());
445+
serializedJson = Encoding.UTF8.GetString(stream.ToArray());
446+
}
447+
448+
var doc = JsonDocument.Parse(serializedJson);
449+
var resourceElement = doc.RootElement.EnumerateArray()
450+
.First(e => e.TryGetProperty("$type", out var typeVal) && typeVal.GetString() == "ResourceType");
451+
452+
// Check for legacy properties
453+
bool hasScopeType = resourceElement.TryGetProperty("scopeType", out _);
454+
bool hasReadOnlyScopes = resourceElement.TryGetProperty("readOnlyScopes", out _);
455+
bool hasFlags = resourceElement.TryGetProperty("flags", out _);
456+
457+
// Verify modern properties are present
458+
bool hasReadableScopes = resourceElement.TryGetProperty("readableScopes", out var readableVal);
459+
bool hasWritableScopes = resourceElement.TryGetProperty("writableScopes", out var writableVal);
460+
461+
// Assert: Modern properties should be present
462+
Assert.IsTrue(hasReadableScopes, "readableScopes should be present in JSON");
463+
Assert.IsTrue(hasWritableScopes, "writableScopes should be present in JSON");
464+
Assert.AreEqual(31, readableVal.GetInt32(), "readableScopes should be 31 (All)");
465+
Assert.AreEqual(31, writableVal.GetInt32(), "writableScopes should be 31 (All)");
466+
467+
// Assert: Legacy properties should NOT be present (they are null and should be omitted)
468+
Assert.IsFalse(hasScopeType,
469+
"scopeType should not be serialized when null (JsonIgnoreCondition.WhenWritingNull)");
470+
Assert.IsFalse(hasReadOnlyScopes,
471+
"readOnlyScopes should not be serialized when null (JsonIgnoreCondition.WhenWritingNull)");
472+
Assert.IsFalse(hasFlags,
473+
"flags should not be serialized when null (JsonIgnoreCondition.WhenWritingNull)");
474+
}
475+
476+
[TestMethod]
477+
public void ResourceType_with_legacy_scopes_should_serialize_and_deserialize_correctly()
478+
{
479+
var factory = new TypeFactory(Enumerable.Empty<TypeBase>());
480+
var stringType = factory.Create(() => new StringType());
481+
var objectType = factory.Create(() => new ObjectType(
482+
name: "legacy@v1",
483+
properties: new Dictionary<string, ObjectTypeProperty>
484+
{
485+
["id"] = new(factory.GetReference(stringType), ObjectTypePropertyFlags.Required, "Resource ID")
486+
},
487+
additionalProperties: null));
488+
489+
// Create with legacy properties
490+
var resourceType = factory.Create(() => new ResourceType(
491+
name: "legacy@v1",
492+
body: factory.GetReference(objectType),
493+
functions: null,
494+
scopeType: ScopeType.ResourceGroup,
495+
readOnlyScopes: null,
496+
flags: ResourceFlags.None));
497+
498+
// Serialize
499+
string serializedJson;
500+
using (var stream = new MemoryStream())
501+
{
502+
TypeSerializer.Serialize(stream, factory.GetTypes());
503+
serializedJson = Encoding.UTF8.GetString(stream.ToArray());
504+
}
505+
506+
var doc = JsonDocument.Parse(serializedJson);
507+
var resourceElement = doc.RootElement.EnumerateArray()
508+
.First(e => e.TryGetProperty("$type", out var typeVal) && typeVal.GetString() == "ResourceType");
509+
510+
// Deserialize
511+
TypeBase[] deserializedTypes;
512+
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(serializedJson)))
513+
{
514+
deserializedTypes = TypeSerializer.Deserialize(stream);
515+
}
516+
517+
var deserialized = deserializedTypes.OfType<ResourceType>().First();
518+
519+
Assert.AreEqual(ScopeType.ResourceGroup, deserialized.ReadableScopes);
520+
Assert.AreEqual(ScopeType.ResourceGroup, deserialized.WritableScopes);
521+
}
522+
523+
[TestMethod]
524+
public void ResourceType_modern_scopes_roundtrip_preserves_values()
525+
{
526+
var factory = new TypeFactory(Enumerable.Empty<TypeBase>());
527+
var stringType = factory.Create(() => new StringType());
528+
var objectType = factory.Create(() => new ObjectType(
529+
name: "roundtrip@v1",
530+
properties: new Dictionary<string, ObjectTypeProperty>
531+
{
532+
["name"] = new(factory.GetReference(stringType), ObjectTypePropertyFlags.Required, "Name")
533+
},
534+
additionalProperties: null));
535+
536+
// Create with modern properties
537+
var original = factory.Create(() => new ResourceType(
538+
name: "roundtrip@v1",
539+
body: factory.GetReference(objectType),
540+
functions: null,
541+
writableScopes_in: ScopeType.Subscription | ScopeType.ResourceGroup,
542+
readableScopes_in: ScopeType.All));
543+
544+
// Serialize
545+
string serializedJson;
546+
using (var stream = new MemoryStream())
547+
{
548+
TypeSerializer.Serialize(stream, factory.GetTypes());
549+
serializedJson = Encoding.UTF8.GetString(stream.ToArray());
550+
}
551+
552+
// Deserialize
553+
TypeBase[] deserializedTypes;
554+
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(serializedJson)))
555+
{
556+
deserializedTypes = TypeSerializer.Deserialize(stream);
557+
}
558+
559+
var deserialized = deserializedTypes.OfType<ResourceType>().First();
560+
561+
// Verify scopes match exactly
562+
Assert.AreEqual(original.ReadableScopes, deserialized.ReadableScopes,
563+
"ReadableScopes should be preserved through roundtrip");
564+
Assert.AreEqual(original.WritableScopes, deserialized.WritableScopes,
565+
"WritableScopes should be preserved through roundtrip");
566+
Assert.AreEqual(ScopeType.All, deserialized.ReadableScopes);
567+
Assert.AreEqual(ScopeType.Subscription | ScopeType.ResourceGroup, deserialized.WritableScopes);
568+
}
569+
417570
private static Stream BuildStream(Action<Stream> writeFunc)
418571
{
419572
var memoryStream = new MemoryStream();

src/Bicep.Types/Serialization/TypeSerializer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.IO;
44
using System.Linq;
55
using System.Text.Json;
6+
using System.Text.Json.Serialization;
67
using System.Text.Json.Serialization.Metadata;
78
using Azure.Bicep.Types.Concrete;
89
using Azure.Bicep.Types.Index;
@@ -23,6 +24,7 @@ public static JsonSerializerOptions GetSerializerOptions(TypeFactory? factory =
2324
},
2425
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
2526
WriteIndented = true,
27+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
2628
TypeInfoResolver = TypeJsonContext.Default,
2729
};
2830
}

0 commit comments

Comments
 (0)