Skip to content
This repository was archived by the owner on Nov 11, 2025. It is now read-only.

Commit ef92cc1

Browse files
Copilotbaywet
andcommitted
Implement OpenAPI 3.2.0 server name field with backward compatibility
Co-authored-by: baywet <[email protected]>
1 parent bc9758e commit ef92cc1

File tree

9 files changed

+368
-3
lines changed

9 files changed

+368
-3
lines changed

src/Microsoft.OpenApi/Models/OpenApiServer.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Text.Json.Nodes;
67

78
namespace Microsoft.OpenApi
89
{
@@ -16,6 +17,12 @@ public class OpenApiServer : IOpenApiSerializable, IOpenApiExtensible
1617
/// </summary>
1718
public string? Description { get; set; }
1819

20+
/// <summary>
21+
/// An optional string identifying the server. This MUST be unique across servers in the same document.
22+
/// Note: This field is supported in OpenAPI 3.2.0+. For earlier versions, it will be serialized as x-oai-name extension.
23+
/// </summary>
24+
public string? Name { get; set; }
25+
1926
/// <summary>
2027
/// REQUIRED. A URL to the target host. This URL supports Server Variables and MAY be relative,
2128
/// to indicate that the host location is relative to the location where the OpenAPI document is being served.
@@ -44,6 +51,7 @@ public OpenApiServer() { }
4451
public OpenApiServer(OpenApiServer server)
4552
{
4653
Description = server?.Description ?? Description;
54+
Name = server?.Name ?? Name;
4755
Url = server?.Url ?? Url;
4856
Variables = server?.Variables != null ? new Dictionary<string, OpenApiServerVariable>(server.Variables) : null;
4957
Extensions = server?.Extensions != null ? new Dictionary<string, IOpenApiExtension>(server.Extensions) : null;
@@ -86,14 +94,32 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
8694
// url
8795
writer.WriteProperty(OpenApiConstants.Url, Url);
8896

97+
// name - serialize as native field for v3.2+ or as extension for earlier versions
98+
if (!string.IsNullOrEmpty(Name))
99+
{
100+
if (version == OpenApiSpecVersion.OpenApi3_2)
101+
{
102+
writer.WriteProperty(OpenApiConstants.Name, Name);
103+
}
104+
}
105+
89106
// description
90107
writer.WriteProperty(OpenApiConstants.Description, Description);
91108

92109
// variables
93110
writer.WriteOptionalMap(OpenApiConstants.Variables, Variables, callback);
94111

95112
// specification extensions
96-
writer.WriteExtensions(Extensions, version);
113+
var extensionsToWrite = Extensions;
114+
115+
// For non-v3.2 versions, add x-oai-name extension if Name is present
116+
if (!string.IsNullOrEmpty(Name) && version != OpenApiSpecVersion.OpenApi3_2)
117+
{
118+
extensionsToWrite = new Dictionary<string, IOpenApiExtension>(Extensions ?? new Dictionary<string, IOpenApiExtension>());
119+
extensionsToWrite["x-oai-name"] = new JsonNodeExtension(JsonValue.Create(Name)!);
120+
}
121+
122+
writer.WriteExtensions(extensionsToWrite, version);
97123

98124
writer.WriteEndObject();
99125
}

src/Microsoft.OpenApi/Reader/V3/OpenApiServerDeserializer.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,17 @@ internal static partial class OpenApiV3Deserializer
2929

3030
private static readonly PatternFieldMap<OpenApiServer> _serverPatternFields = new()
3131
{
32-
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p,n))}
32+
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) =>
33+
{
34+
if (p.Equals("x-oai-name", StringComparison.OrdinalIgnoreCase))
35+
{
36+
o.Name = n.GetScalarValue();
37+
}
38+
else
39+
{
40+
o.AddExtension(p, LoadExtension(p,n));
41+
}
42+
}}
3343
};
3444

3545
public static OpenApiServer LoadServer(ParseNode node, OpenApiDocument hostDocument)

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,17 @@ internal static partial class OpenApiV31Deserializer
3535

3636
private static readonly PatternFieldMap<OpenApiServer> _serverPatternFields = new()
3737
{
38-
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p,n))}
38+
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) =>
39+
{
40+
if (p.Equals("x-oai-name", StringComparison.OrdinalIgnoreCase))
41+
{
42+
o.Name = n.GetScalarValue();
43+
}
44+
else
45+
{
46+
o.AddExtension(p, LoadExtension(p,n));
47+
}
48+
}}
3949
};
4050

4151
public static OpenApiServer LoadServer(ParseNode node, OpenApiDocument hostDocument)

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ internal static partial class OpenApiV32Deserializer
2525
o.Description = n.GetScalarValue();
2626
}
2727
},
28+
{
29+
"name", (o, n, _) =>
30+
{
31+
o.Name = n.GetScalarValue();
32+
}
33+
},
2834
{
2935
"variables", (o, n, t) =>
3036
{
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.IO;
2+
using Microsoft.OpenApi.Reader;
3+
using Microsoft.OpenApi.Reader.V31;
4+
using Microsoft.OpenApi.YamlReader;
5+
using SharpYaml.Serialization;
6+
using Xunit;
7+
8+
namespace Microsoft.OpenApi.Readers.Tests.V31Tests
9+
{
10+
public class OpenApiServerTests
11+
{
12+
[Fact]
13+
public void ParseServerWithXOaiNameExtensionShouldSucceed()
14+
{
15+
var input =
16+
"""
17+
url: https://dev.example.com
18+
description: Development server
19+
x-oai-name: dev-server
20+
""";
21+
22+
var yamlStream = new YamlStream();
23+
yamlStream.Load(new StringReader(input));
24+
var yamlNode = yamlStream.Documents[0].RootNode;
25+
26+
var diagnostic = new OpenApiDiagnostic();
27+
var context = new ParsingContext(diagnostic);
28+
29+
var asJsonNode = yamlNode.ToJsonNode();
30+
var node = new MapNode(context, asJsonNode);
31+
32+
// Act
33+
var openApiServer = OpenApiV31Deserializer.LoadServer(node, new());
34+
35+
// Assert
36+
Assert.Equal("https://dev.example.com", openApiServer.Url);
37+
Assert.Equal("dev-server", openApiServer.Name);
38+
Assert.Equal("Development server", openApiServer.Description);
39+
// The x-oai-name extension should not be in the extensions collection since it's parsed to Name property
40+
Assert.Null(openApiServer.Extensions);
41+
}
42+
43+
[Fact]
44+
public void ParseServerWithOtherExtensionShouldKeepExtension()
45+
{
46+
var input =
47+
"""
48+
url: https://example.com
49+
description: Sample server
50+
x-custom-extension: custom-value
51+
""";
52+
53+
var yamlStream = new YamlStream();
54+
yamlStream.Load(new StringReader(input));
55+
var yamlNode = yamlStream.Documents[0].RootNode;
56+
57+
var diagnostic = new OpenApiDiagnostic();
58+
var context = new ParsingContext(diagnostic);
59+
60+
var asJsonNode = yamlNode.ToJsonNode();
61+
var node = new MapNode(context, asJsonNode);
62+
63+
// Act
64+
var openApiServer = OpenApiV31Deserializer.LoadServer(node, new());
65+
66+
// Assert
67+
Assert.Equal("https://example.com", openApiServer.Url);
68+
Assert.Null(openApiServer.Name);
69+
Assert.Equal("Sample server", openApiServer.Description);
70+
Assert.NotNull(openApiServer.Extensions);
71+
Assert.Single(openApiServer.Extensions);
72+
Assert.True(openApiServer.Extensions.ContainsKey("x-custom-extension"));
73+
}
74+
}
75+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System.IO;
2+
using Microsoft.OpenApi.Reader;
3+
using Microsoft.OpenApi.Reader.V32;
4+
using Microsoft.OpenApi.YamlReader;
5+
using SharpYaml.Serialization;
6+
using Xunit;
7+
8+
namespace Microsoft.OpenApi.Readers.Tests.V32Tests
9+
{
10+
public class OpenApiServerTests
11+
{
12+
[Fact]
13+
public void ParseServerWithNameShouldSucceed()
14+
{
15+
var input =
16+
"""
17+
url: https://dev.example.com
18+
name: dev-server
19+
description: Development server
20+
""";
21+
22+
var yamlStream = new YamlStream();
23+
yamlStream.Load(new StringReader(input));
24+
var yamlNode = yamlStream.Documents[0].RootNode;
25+
26+
var diagnostic = new OpenApiDiagnostic();
27+
var context = new ParsingContext(diagnostic);
28+
29+
var asJsonNode = yamlNode.ToJsonNode();
30+
var node = new MapNode(context, asJsonNode);
31+
32+
// Act
33+
var openApiServer = OpenApiV32Deserializer.LoadServer(node, new());
34+
35+
// Assert
36+
Assert.Equal("https://dev.example.com", openApiServer.Url);
37+
Assert.Equal("dev-server", openApiServer.Name);
38+
Assert.Equal("Development server", openApiServer.Description);
39+
}
40+
41+
[Fact]
42+
public void ParseServerWithoutNameShouldSucceed()
43+
{
44+
var input =
45+
"""
46+
url: https://example.com
47+
description: Sample server
48+
""";
49+
50+
var yamlStream = new YamlStream();
51+
yamlStream.Load(new StringReader(input));
52+
var yamlNode = yamlStream.Documents[0].RootNode;
53+
54+
var diagnostic = new OpenApiDiagnostic();
55+
var context = new ParsingContext(diagnostic);
56+
57+
var asJsonNode = yamlNode.ToJsonNode();
58+
var node = new MapNode(context, asJsonNode);
59+
60+
// Act
61+
var openApiServer = OpenApiV32Deserializer.LoadServer(node, new());
62+
63+
// Assert
64+
Assert.Equal("https://example.com", openApiServer.Url);
65+
Assert.Null(openApiServer.Name);
66+
Assert.Equal("Sample server", openApiServer.Description);
67+
}
68+
}
69+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.IO;
2+
using Microsoft.OpenApi.Reader;
3+
using Microsoft.OpenApi.Reader.V3;
4+
using Microsoft.OpenApi.YamlReader;
5+
using SharpYaml.Serialization;
6+
using Xunit;
7+
8+
namespace Microsoft.OpenApi.Readers.Tests.V3Tests
9+
{
10+
public class OpenApiServerTests
11+
{
12+
[Fact]
13+
public void ParseServerWithXOaiNameExtensionShouldSucceed()
14+
{
15+
var input =
16+
"""
17+
url: https://dev.example.com
18+
description: Development server
19+
x-oai-name: dev-server
20+
""";
21+
22+
var yamlStream = new YamlStream();
23+
yamlStream.Load(new StringReader(input));
24+
var yamlNode = yamlStream.Documents[0].RootNode;
25+
26+
var diagnostic = new OpenApiDiagnostic();
27+
var context = new ParsingContext(diagnostic);
28+
29+
var asJsonNode = yamlNode.ToJsonNode();
30+
var node = new MapNode(context, asJsonNode);
31+
32+
// Act
33+
var openApiServer = OpenApiV3Deserializer.LoadServer(node, new());
34+
35+
// Assert
36+
Assert.Equal("https://dev.example.com", openApiServer.Url);
37+
Assert.Equal("dev-server", openApiServer.Name);
38+
Assert.Equal("Development server", openApiServer.Description);
39+
// The x-oai-name extension should not be in the extensions collection since it's parsed to Name property
40+
Assert.Null(openApiServer.Extensions);
41+
}
42+
43+
[Fact]
44+
public void ParseServerWithOtherExtensionShouldKeepExtension()
45+
{
46+
var input =
47+
"""
48+
url: https://example.com
49+
description: Sample server
50+
x-custom-extension: custom-value
51+
""";
52+
53+
var yamlStream = new YamlStream();
54+
yamlStream.Load(new StringReader(input));
55+
var yamlNode = yamlStream.Documents[0].RootNode;
56+
57+
var diagnostic = new OpenApiDiagnostic();
58+
var context = new ParsingContext(diagnostic);
59+
60+
var asJsonNode = yamlNode.ToJsonNode();
61+
var node = new MapNode(context, asJsonNode);
62+
63+
// Act
64+
var openApiServer = OpenApiV3Deserializer.LoadServer(node, new());
65+
66+
// Assert
67+
Assert.Equal("https://example.com", openApiServer.Url);
68+
Assert.Null(openApiServer.Name);
69+
Assert.Equal("Sample server", openApiServer.Description);
70+
Assert.NotNull(openApiServer.Extensions);
71+
Assert.Single(openApiServer.Extensions);
72+
Assert.True(openApiServer.Extensions.ContainsKey("x-custom-extension"));
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)