Skip to content

Commit 74a90eb

Browse files
authored
Handle null in collections (list/map) (#106)
* - Null object in collections is handled explicitly. Otherwise, operating an action on null (e.g. calling SerializeAsV3) has potential to yield null reference exception. - This is sort of a grey area since we will only encounter this for a "broken" object (e.g. a null schema in the Schemas map in Components - see the written unit tests), but I think it's a good idea to have this safeguard. Getting a generic null reference exception is a bad experience for our customer in any situation, I believe. - Unit test for Components
1 parent 41bd285 commit 74a90eb

File tree

5 files changed

+289
-6
lines changed

5 files changed

+289
-6
lines changed

src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<Company>Microsoft</Company>
77
<Product>Microsoft.OpenApi.Readers</Product>
88
<PackageId>Microsoft.OpenApi.Readers</PackageId>
9-
<Version>1.0.0-beta006</Version>
9+
<Version>1.0.0-beta007</Version>
1010
<Description>OpenAPI.NET Readers for JSON and YAML documents</Description>
1111
<AssemblyName>Microsoft.OpenApi.Readers</AssemblyName>
1212
<RootNamespace>Microsoft.OpenApi.Readers</RootNamespace>

src/Microsoft.OpenApi/Microsoft.OpenApi.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<Company>Microsoft</Company>
77
<Product>Microsoft.OpenApi</Product>
88
<PackageId>Microsoft.OpenApi</PackageId>
9-
<Version>1.0.0-beta006</Version>
9+
<Version>1.0.0-beta007</Version>
1010
<Description>.NET models and JSON/YAML writers for OpenAPI specification</Description>
1111
<AssemblyName>Microsoft.OpenApi</AssemblyName>
1212
<RootNamespace>Microsoft.OpenApi</RootNamespace>

src/Microsoft.OpenApi/Writers/OpenApiJsonWriter.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ public override void WriteValue(string value)
178178
/// </summary>
179179
public override void WriteNull()
180180
{
181+
WriteValueSeparator();
182+
181183
Writer.Write("null");
182184
}
183185

src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,14 @@ private static void WriteCollectionInternal<T>(
302302
{
303303
foreach (var item in elements)
304304
{
305-
action(writer, item);
305+
if (item != null)
306+
{
307+
action(writer, item);
308+
}
309+
else
310+
{
311+
writer.WriteNull();
312+
}
306313
}
307314
}
308315

@@ -325,7 +332,14 @@ private static void WriteMapInternal<T>(
325332
foreach (var item in elements)
326333
{
327334
writer.WritePropertyName(item.Key);
328-
action(writer, item.Value);
335+
if (item.Value != null)
336+
{
337+
action(writer, item.Value);
338+
}
339+
else
340+
{
341+
writer.WriteNull();
342+
}
329343
}
330344
}
331345

test/Microsoft.OpenApi.Tests/Models/OpenApiComponentsTests.cs

Lines changed: 269 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,279 @@
33
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
44
// ------------------------------------------------------------
55

6+
using System;
7+
using System.Collections.Generic;
8+
using FluentAssertions;
9+
using Microsoft.OpenApi.Extensions;
610
using Microsoft.OpenApi.Models;
11+
using Xunit;
12+
using Xunit.Abstractions;
713

814
namespace Microsoft.OpenApi.Tests.Models
915
{
1016
public class OpenApiComponentsTests
1117
{
12-
public static OpenApiComponents basicComponents = new OpenApiComponents();
18+
public static OpenApiComponents AdvancedComponents = new OpenApiComponents
19+
{
20+
Schemas = new Dictionary<string, OpenApiSchema>
21+
{
22+
["schema1"] = new OpenApiSchema
23+
{
24+
Properties = new Dictionary<string, OpenApiSchema>
25+
{
26+
["property2"] = new OpenApiSchema
27+
{
28+
Type = "integer"
29+
},
30+
["property3"] = new OpenApiSchema
31+
{
32+
Type = "string",
33+
MaxLength = 15
34+
}
35+
},
36+
},
37+
},
38+
SecuritySchemes = new Dictionary<string, OpenApiSecurityScheme>
39+
{
40+
["securityScheme1"] = new OpenApiSecurityScheme
41+
{
42+
Description = "description1",
43+
Type = SecuritySchemeType.OAuth2,
44+
Flows = new OpenApiOAuthFlows
45+
{
46+
Implicit = new OpenApiOAuthFlow
47+
{
48+
Scopes = new Dictionary<string, string>
49+
{
50+
["operation1:object1"] = "operation 1 on object 1",
51+
["operation2:object2"] = "operation 2 on object 2"
52+
},
53+
AuthorizationUrl = new Uri("https://example.com/api/oauth")
54+
}
55+
}
56+
},
57+
["securityScheme2"] = new OpenApiSecurityScheme
58+
{
59+
Description = "description1",
60+
Type = SecuritySchemeType.OpenIdConnect,
61+
Scheme = "openIdConnectUrl",
62+
OpenIdConnectUrl = new Uri("https://example.com/openIdConnect")
63+
}
64+
}
65+
};
66+
67+
public static OpenApiComponents BasicComponents = new OpenApiComponents();
68+
69+
public static OpenApiComponents BrokenComponents = new OpenApiComponents
70+
{
71+
Schemas = new Dictionary<string, OpenApiSchema>
72+
{
73+
["schema1"] = new OpenApiSchema
74+
{
75+
Type = "string"
76+
},
77+
["schema2"] = null,
78+
["schema3"] = null,
79+
["schema4"] = new OpenApiSchema
80+
{
81+
Type = "string",
82+
AllOf = new List<OpenApiSchema>()
83+
{
84+
null,
85+
null,
86+
new OpenApiSchema()
87+
{
88+
Type = "string"
89+
},
90+
null,
91+
null
92+
}
93+
}
94+
}
95+
};
96+
97+
private readonly ITestOutputHelper _output;
98+
99+
public OpenApiComponentsTests(ITestOutputHelper output)
100+
{
101+
_output = output;
102+
}
103+
104+
[Fact]
105+
public void SerializeBasicComponentsAsJsonWorks()
106+
{
107+
// Arrange
108+
var expected = @"{ }";
109+
110+
// Act
111+
var actual = BasicComponents.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0_0);
112+
113+
// Assert
114+
actual = actual.MakeLineBreaksEnvironmentNeutral();
115+
expected = expected.MakeLineBreaksEnvironmentNeutral();
116+
actual.Should().Be(expected);
117+
}
118+
119+
[Fact]
120+
public void SerializeBasicComponentsAsYamlWorks()
121+
{
122+
// Arrange
123+
var expected = @"{ }";
124+
125+
// Act
126+
var actual = BasicComponents.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0_0);
127+
128+
// Assert
129+
actual = actual.MakeLineBreaksEnvironmentNeutral();
130+
expected = expected.MakeLineBreaksEnvironmentNeutral();
131+
actual.Should().Be(expected);
132+
}
133+
134+
[Fact]
135+
public void SerializeAdvancedComponentsAsJsonWorks()
136+
{
137+
// Arrange
138+
var expected = @"{
139+
""schemas"": {
140+
""schema1"": {
141+
""properties"": {
142+
""property2"": {
143+
""type"": ""integer""
144+
},
145+
""property3"": {
146+
""maxLength"": 15,
147+
""type"": ""string""
148+
}
149+
}
150+
}
151+
},
152+
""securitySchemes"": {
153+
""securityScheme1"": {
154+
""type"": ""oauth2"",
155+
""description"": ""description1"",
156+
""flows"": {
157+
""implicit"": {
158+
""authorizationUrl"": ""https://example.com/api/oauth"",
159+
""scopes"": {
160+
""operation1:object1"": ""operation 1 on object 1"",
161+
""operation2:object2"": ""operation 2 on object 2""
162+
}
163+
}
164+
}
165+
},
166+
""securityScheme2"": {
167+
""type"": ""openIdConnect"",
168+
""description"": ""description1"",
169+
""openIdConnectUrl"": ""https://example.com/openIdConnect""
170+
}
171+
}
172+
}";
173+
174+
// Act
175+
var actual = AdvancedComponents.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0_0);
176+
177+
// Assert
178+
actual = actual.MakeLineBreaksEnvironmentNeutral();
179+
expected = expected.MakeLineBreaksEnvironmentNeutral();
180+
actual.Should().Be(expected);
181+
}
182+
183+
[Fact]
184+
public void SerializeAdvancedComponentsAsYamlWorks()
185+
{
186+
// Arrange
187+
var expected = @"schemas:
188+
schema1:
189+
properties:
190+
property2:
191+
type: integer
192+
property3:
193+
maxLength: 15
194+
type: string
195+
securitySchemes:
196+
securityScheme1:
197+
type: oauth2
198+
description: description1
199+
flows:
200+
implicit:
201+
authorizationUrl: https://example.com/api/oauth
202+
scopes:
203+
operation1:object1: operation 1 on object 1
204+
operation2:object2: operation 2 on object 2
205+
securityScheme2:
206+
type: openIdConnect
207+
description: description1
208+
openIdConnectUrl: https://example.com/openIdConnect";
209+
210+
// Act
211+
var actual = AdvancedComponents.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0_0);
212+
213+
// Assert
214+
actual = actual.MakeLineBreaksEnvironmentNeutral();
215+
expected = expected.MakeLineBreaksEnvironmentNeutral();
216+
actual.Should().Be(expected);
217+
}
218+
219+
[Fact]
220+
public void SerializeBrokenComponentsAsJsonWorks()
221+
{
222+
// Arrange
223+
var expected = @"{
224+
""schemas"": {
225+
""schema1"": {
226+
""type"": ""string""
227+
},
228+
""schema2"": null,
229+
""schema3"": null,
230+
""schema4"": {
231+
""type"": ""string"",
232+
""allOf"": [
233+
null,
234+
null,
235+
{
236+
""type"": ""string""
237+
},
238+
null,
239+
null
240+
]
241+
}
242+
}
243+
}";
244+
245+
// Act
246+
var actual = BrokenComponents.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0_0);
247+
248+
// Assert
249+
actual = actual.MakeLineBreaksEnvironmentNeutral();
250+
expected = expected.MakeLineBreaksEnvironmentNeutral();
251+
actual.Should().Be(expected);
252+
}
253+
254+
[Fact]
255+
public void SerializeBrokenComponentsAsYamlWorks()
256+
{
257+
// Arrange
258+
var expected = @"schemas:
259+
schema1:
260+
type: string
261+
schema2:
262+
schema3:
263+
schema4:
264+
type: string
265+
allOf:
266+
-
267+
-
268+
- type: string
269+
-
270+
- ";
271+
272+
// Act
273+
var actual = BrokenComponents.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0_0);
274+
275+
// Assert
276+
actual = actual.MakeLineBreaksEnvironmentNeutral();
277+
expected = expected.MakeLineBreaksEnvironmentNeutral();
278+
actual.Should().Be(expected);
279+
}
13280
}
14-
}
281+
}

0 commit comments

Comments
 (0)