Skip to content

Commit 83e7587

Browse files
committed
fix: address PR review feedback
- Replace OpenApiSpecVersion branching with Action<IOpenApiWriter, IOpenApiSerializable> callback for future-proof serialization - Add $defs context segment + try/catch error handling in V31/V32 $defs parsing (mirrors ParseField error pattern) - Convert all FluentAssertions calls to Assert.* per repo convention Ref: #2896
1 parent af0686a commit 83e7587

5 files changed

Lines changed: 142 additions & 115 deletions

File tree

src/Microsoft.OpenApi/Models/JsonSchemaReference.cs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,14 @@ public JsonSchemaReference(JsonSchemaReference reference) : base(reference)
130130
/// <inheritdoc/>
131131
protected override void SerializeAdditionalV31Properties(IOpenApiWriter writer)
132132
{
133-
SerializeAdditionalV3XProperties(writer, OpenApiSpecVersion.OpenApi3_1, base.SerializeAdditionalV31Properties);
133+
SerializeAdditionalV3XProperties(writer, (w, e) => e.SerializeAsV31(w), base.SerializeAdditionalV31Properties);
134134
}
135135
/// <inheritdoc/>
136136
protected override void SerializeAdditionalV32Properties(IOpenApiWriter writer)
137137
{
138-
SerializeAdditionalV3XProperties(writer, OpenApiSpecVersion.OpenApi3_2, base.SerializeAdditionalV32Properties);
138+
SerializeAdditionalV3XProperties(writer, (w, e) => e.SerializeAsV32(w), base.SerializeAdditionalV32Properties);
139139
}
140-
private void SerializeAdditionalV3XProperties(IOpenApiWriter writer, OpenApiSpecVersion version, Action<IOpenApiWriter> baseSerializer)
140+
private void SerializeAdditionalV3XProperties(IOpenApiWriter writer, Action<IOpenApiWriter, IOpenApiSerializable> serializeCallback, Action<IOpenApiWriter> baseSerializer)
141141
{
142142
if (Type != ReferenceType.Schema) throw new InvalidOperationException(
143143
$"JsonSchemaReference can only be serialized for ReferenceType.Schema, but was {Type}.");
@@ -149,14 +149,7 @@ private void SerializeAdditionalV3XProperties(IOpenApiWriter writer, OpenApiSpec
149149
writer.WriteProperty(OpenApiConstants.DollarSchema, Schema?.ToString());
150150
writer.WriteProperty(OpenApiConstants.Comment, Comment);
151151
writer.WriteOptionalMap(OpenApiConstants.Vocabulary, Vocabulary, (w, s) => w.WriteValue(s));
152-
if (version == OpenApiSpecVersion.OpenApi3_1)
153-
{
154-
writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, (w, s) => s.SerializeAsV31(w));
155-
}
156-
else
157-
{
158-
writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, (w, s) => s.SerializeAsV32(w));
159-
}
152+
writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, serializeCallback);
160153
writer.WriteProperty(OpenApiConstants.Anchor, Anchor);
161154
writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef);
162155
writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor);

src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -458,19 +458,36 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
458458
if (jsonObject.TryGetPropertyValue(OpenApiConstants.Defs, out var defsNode) && defsNode is JsonObject defsObj)
459459
{
460460
var defs = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
461-
foreach (var kvp in defsObj)
461+
context.StartObject(OpenApiConstants.Defs);
462+
try
462463
{
463-
if (kvp.Value is null) continue;
464-
context.StartObject(kvp.Key);
465-
try
464+
foreach (var kvp in defsObj)
466465
{
467-
defs[kvp.Key] = LoadSchema(kvp.Value, hostDocument, context);
468-
}
469-
finally
470-
{
471-
context.EndObject();
466+
if (kvp.Value is null) continue;
467+
try
468+
{
469+
context.StartObject(kvp.Key);
470+
defs[kvp.Key] = LoadSchema(kvp.Value, hostDocument, context);
471+
}
472+
catch (OpenApiReaderException ex)
473+
{
474+
context.Diagnostic.Errors.Add(new(ex));
475+
}
476+
catch (OpenApiException ex)
477+
{
478+
ex.Pointer = context.GetLocation();
479+
context.Diagnostic.Errors.Add(new(ex));
480+
}
481+
finally
482+
{
483+
context.EndObject();
484+
}
472485
}
473486
}
487+
finally
488+
{
489+
context.EndObject();
490+
}
474491
if (defs.Count > 0)
475492
{
476493
result.Reference.Definitions = defs;

src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -458,19 +458,36 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
458458
if (jsonObject.TryGetPropertyValue(OpenApiConstants.Defs, out var defsNode) && defsNode is JsonObject defsObj)
459459
{
460460
var defs = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
461-
foreach (var kvp in defsObj)
461+
context.StartObject(OpenApiConstants.Defs);
462+
try
462463
{
463-
if (kvp.Value is null) continue;
464-
context.StartObject(kvp.Key);
465-
try
464+
foreach (var kvp in defsObj)
466465
{
467-
defs[kvp.Key] = LoadSchema(kvp.Value, hostDocument, context);
468-
}
469-
finally
470-
{
471-
context.EndObject();
466+
if (kvp.Value is null) continue;
467+
try
468+
{
469+
context.StartObject(kvp.Key);
470+
defs[kvp.Key] = LoadSchema(kvp.Value, hostDocument, context);
471+
}
472+
catch (OpenApiReaderException ex)
473+
{
474+
context.Diagnostic.Errors.Add(new(ex));
475+
}
476+
catch (OpenApiException ex)
477+
{
478+
ex.Pointer = context.GetLocation();
479+
context.Diagnostic.Errors.Add(new(ex));
480+
}
481+
finally
482+
{
483+
context.EndObject();
484+
}
472485
}
473486
}
487+
finally
488+
{
489+
context.EndObject();
490+
}
474491
if (defs.Count > 0)
475492
{
476493
result.Reference.Definitions = defs;

test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs

Lines changed: 52 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -969,12 +969,12 @@ public async Task ParseSchemaReferencePreservesJsonSchema2020KeywordSiblings()
969969
var referencing = result.Document.Components!.Schemas["Referencing"];
970970

971971
// Assert — siblings are preserved on the OpenApiSchemaReference
972-
referencing.Should().BeOfType<OpenApiSchemaReference>();
973-
referencing.Description.Should().Be("Sibling description");
974-
referencing.DynamicAnchor.Should().Be("anchor");
975-
referencing.Definitions.Should().NotBeNull();
976-
referencing.Definitions!.Should().ContainKey("sibling");
977-
referencing.Definitions["sibling"].DynamicAnchor.Should().Be("inner");
972+
Assert.IsType<OpenApiSchemaReference>(referencing);
973+
Assert.Equal("Sibling description", referencing.Description);
974+
Assert.Equal("anchor", referencing.DynamicAnchor);
975+
Assert.NotNull(referencing.Definitions);
976+
Assert.True(referencing.Definitions!.ContainsKey("sibling"));
977+
Assert.Equal("inner", referencing.Definitions["sibling"].DynamicAnchor);
978978
}
979979

980980
[Fact]
@@ -1015,11 +1015,11 @@ public async Task SerializeSchemaReferencePreservesJsonSchema2020KeywordSiblings
10151015
var roundTripResult = await OpenApiDocument.LoadAsync(roundTripStream, "yaml", SettingsFixture.ReaderSettings);
10161016
var referencing = roundTripResult.Document.Components!.Schemas["Referencing"];
10171017

1018-
referencing.Should().BeOfType<OpenApiSchemaReference>();
1019-
referencing.DynamicAnchor.Should().Be("anchor");
1020-
referencing.Definitions.Should().NotBeNull();
1021-
referencing.Definitions!.Should().ContainKey("itemType");
1022-
referencing.Definitions["itemType"].DynamicAnchor.Should().Be("itemType");
1018+
Assert.IsType<OpenApiSchemaReference>(referencing);
1019+
Assert.Equal("anchor", referencing.DynamicAnchor);
1020+
Assert.NotNull(referencing.Definitions);
1021+
Assert.True(referencing.Definitions!.ContainsKey("itemType"));
1022+
Assert.Equal("itemType", referencing.Definitions["itemType"].DynamicAnchor);
10231023
}
10241024

10251025
[Fact]
@@ -1051,12 +1051,12 @@ public async Task ParseSchemaReferencePreservesScalarKeywordSiblings()
10511051
var referencing = result.Document.Components!.Schemas["Referencing"];
10521052

10531053
// Assert
1054-
referencing.Should().BeOfType<OpenApiSchemaReference>();
1055-
referencing.Id.Should().Be("https://example.com/referencing.json");
1056-
referencing.Schema.Should().Be(new Uri("https://json-schema.org/draft/2020-12/schema"));
1057-
referencing.Comment.Should().Be("A comment sibling");
1058-
((IOpenApiSchemaMissingProperties)referencing).Anchor.Should().Be("myAnchor");
1059-
referencing.DynamicRef.Should().Be("#myAnchor");
1054+
Assert.IsType<OpenApiSchemaReference>(referencing);
1055+
Assert.Equal("https://example.com/referencing.json", referencing.Id);
1056+
Assert.Equal(new Uri("https://json-schema.org/draft/2020-12/schema"), referencing.Schema);
1057+
Assert.Equal("A comment sibling", referencing.Comment);
1058+
Assert.Equal("myAnchor", ((IOpenApiSchemaMissingProperties)referencing).Anchor);
1059+
Assert.Equal("#myAnchor", referencing.DynamicRef);
10601060
}
10611061

10621062
[Fact]
@@ -1094,12 +1094,12 @@ public async Task SerializeSchemaReferencePreservesScalarKeywordSiblings()
10941094
var roundTripResult = await OpenApiDocument.LoadAsync(roundTripStream, "yaml", SettingsFixture.ReaderSettings);
10951095
var referencing = roundTripResult.Document.Components!.Schemas["Referencing"];
10961096

1097-
referencing.Should().BeOfType<OpenApiSchemaReference>();
1098-
referencing.Id.Should().Be("https://example.com/referencing.json");
1099-
referencing.Schema.Should().Be(new Uri("https://json-schema.org/draft/2020-12/schema"));
1100-
referencing.Comment.Should().Be("A comment sibling");
1101-
((IOpenApiSchemaMissingProperties)referencing).Anchor.Should().Be("myAnchor");
1102-
referencing.DynamicRef.Should().Be("#myAnchor");
1097+
Assert.IsType<OpenApiSchemaReference>(referencing);
1098+
Assert.Equal("https://example.com/referencing.json", referencing.Id);
1099+
Assert.Equal(new Uri("https://json-schema.org/draft/2020-12/schema"), referencing.Schema);
1100+
Assert.Equal("A comment sibling", referencing.Comment);
1101+
Assert.Equal("myAnchor", ((IOpenApiSchemaMissingProperties)referencing).Anchor);
1102+
Assert.Equal("#myAnchor", referencing.DynamicRef);
11031103
}
11041104

11051105
[Fact]
@@ -1129,11 +1129,11 @@ public async Task ParseSchemaReferencePreservesVocabularySibling()
11291129
var referencing = result.Document.Components!.Schemas["Referencing"];
11301130

11311131
// Assert
1132-
referencing.Should().BeOfType<OpenApiSchemaReference>();
1133-
referencing.Vocabulary.Should().NotBeNull();
1134-
referencing.Vocabulary!.Should().HaveCount(2);
1135-
referencing.Vocabulary["https://json-schema.org/draft/2020-12/vocab/core"].Should().BeTrue();
1136-
referencing.Vocabulary["https://json-schema.org/draft/2020-12/vocab/applicator"].Should().BeFalse();
1132+
Assert.IsType<OpenApiSchemaReference>(referencing);
1133+
Assert.NotNull(referencing.Vocabulary);
1134+
Assert.Equal(2, referencing.Vocabulary!.Count);
1135+
Assert.True(referencing.Vocabulary["https://json-schema.org/draft/2020-12/vocab/core"]);
1136+
Assert.False(referencing.Vocabulary["https://json-schema.org/draft/2020-12/vocab/applicator"]);
11371137
}
11381138

11391139
[Fact]
@@ -1179,14 +1179,14 @@ public async Task ParseSchemaReferencePreservesDynamicAnchorInsideDefsInAllOf()
11791179
// allOf[0] is a regular OpenApiSchema (no $ref at top level), so $defs is parsed normally.
11801180
// The nested contentType schema is an OpenApiSchemaReference ($ref: Asset),
11811181
// and its $dynamicAnchor sibling must be preserved.
1182-
assetPaged.AllOf.Should().NotBeNull();
1183-
assetPaged.AllOf!.Count.Should().Be(2);
1182+
Assert.NotNull(assetPaged.AllOf);
1183+
Assert.Equal(2, assetPaged.AllOf!.Count);
11841184
var defsHolder = assetPaged.AllOf[0];
1185-
defsHolder.Definitions.Should().NotBeNull();
1186-
defsHolder.Definitions!.Should().ContainKey("contentType");
1185+
Assert.NotNull(defsHolder.Definitions);
1186+
Assert.True(defsHolder.Definitions!.ContainsKey("contentType"));
11871187
var contentType = defsHolder.Definitions["contentType"];
1188-
contentType.Should().BeOfType<OpenApiSchemaReference>();
1189-
contentType.DynamicAnchor.Should().Be("contentType");
1188+
Assert.IsType<OpenApiSchemaReference>(contentType);
1189+
Assert.Equal("contentType", contentType.DynamicAnchor);
11901190
}
11911191

11921192
[Fact]
@@ -1221,11 +1221,11 @@ public async Task EmptySiblingCollectionsFallThroughToTarget()
12211221
var referencing = result.Document.Components!.Schemas["Referencing"];
12221222

12231223
// Assert — empty siblings fall through to Target's values
1224-
referencing.Should().BeOfType<OpenApiSchemaReference>();
1225-
referencing.Definitions.Should().NotBeNull();
1226-
referencing.Definitions!.Should().ContainKey("targetDef");
1227-
referencing.Vocabulary.Should().NotBeNull();
1228-
referencing.Vocabulary!.Should().ContainKey("https://json-schema.org/draft/2020-12/vocab/core");
1224+
Assert.IsType<OpenApiSchemaReference>(referencing);
1225+
Assert.NotNull(referencing.Definitions);
1226+
Assert.True(referencing.Definitions!.ContainsKey("targetDef"));
1227+
Assert.NotNull(referencing.Vocabulary);
1228+
Assert.True(referencing.Vocabulary!.ContainsKey("https://json-schema.org/draft/2020-12/vocab/core"));
12291229
}
12301230

12311231
[Fact]
@@ -1261,9 +1261,9 @@ public async Task SiblingsOnRefAreDroppedForOpenApi30()
12611261
var referencing = result.Document.Components!.Schemas["Referencing"];
12621262

12631263
// Assert — siblings are dropped for 3.0 (per spec: $ref siblings MUST be ignored)
1264-
referencing.Should().BeOfType<OpenApiSchemaReference>();
1265-
referencing.DynamicAnchor.Should().BeNull();
1266-
referencing.Definitions?.Should().BeNull();
1264+
Assert.IsType<OpenApiSchemaReference>(referencing);
1265+
Assert.Null(referencing.DynamicAnchor);
1266+
Assert.Null(referencing.Definitions);
12671267
}
12681268

12691269
[Fact]
@@ -1299,11 +1299,11 @@ public async Task SerializeSchemaReferencePreservesVocabularySibling()
12991299
var roundTripResult = await OpenApiDocument.LoadAsync(roundTripStream, "yaml", SettingsFixture.ReaderSettings);
13001300
var referencing = roundTripResult.Document.Components!.Schemas["Referencing"];
13011301

1302-
referencing.Should().BeOfType<OpenApiSchemaReference>();
1303-
referencing.Vocabulary.Should().NotBeNull();
1304-
referencing.Vocabulary!.Should().HaveCount(2);
1305-
referencing.Vocabulary["https://json-schema.org/draft/2020-12/vocab/core"].Should().BeTrue();
1306-
referencing.Vocabulary["https://json-schema.org/draft/2020-12/vocab/applicator"].Should().BeFalse();
1302+
Assert.IsType<OpenApiSchemaReference>(referencing);
1303+
Assert.NotNull(referencing.Vocabulary);
1304+
Assert.Equal(2, referencing.Vocabulary!.Count);
1305+
Assert.True(referencing.Vocabulary["https://json-schema.org/draft/2020-12/vocab/core"]);
1306+
Assert.False(referencing.Vocabulary["https://json-schema.org/draft/2020-12/vocab/applicator"]);
13071307
}
13081308

13091309
[Fact]
@@ -1336,11 +1336,11 @@ public async Task CreateShallowCopyPreservesKeywordSiblings()
13361336
var copy = referencing.CreateShallowCopy();
13371337

13381338
// Assert — CreateShallowCopy preserves sibling values via the JsonSchemaReference copy constructor
1339-
copy.Should().BeOfType<OpenApiSchemaReference>();
1340-
copy.DynamicAnchor.Should().Be("anchor");
1341-
copy.Definitions.Should().NotBeNull();
1342-
copy.Definitions!.Should().ContainKey("sibling");
1343-
copy.Definitions["sibling"].DynamicAnchor.Should().Be("inner");
1339+
Assert.IsType<OpenApiSchemaReference>(copy);
1340+
Assert.Equal("anchor", copy.DynamicAnchor);
1341+
Assert.NotNull(copy.Definitions);
1342+
Assert.True(copy.Definitions!.ContainsKey("sibling"));
1343+
Assert.Equal("inner", copy.Definitions["sibling"].DynamicAnchor);
13441344
}
13451345
}
13461346
}

0 commit comments

Comments
 (0)