From 09fb905f901ec366fbd3434c6035abd09adae209 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 28 Mar 2025 13:19:54 -0400 Subject: [PATCH 1/3] fix: changes nullability for media type entries Signed-off-by: Vincent Biret --- .../Models/Interfaces/IOpenApiHeader.cs | 2 +- .../Models/Interfaces/IOpenApiParameter.cs | 2 +- .../Models/Interfaces/IOpenApiRequestBody.cs | 2 +- .../Models/Interfaces/IOpenApiResponse.cs | 2 +- src/Microsoft.OpenApi/Models/OpenApiHeader.cs | 13 ++--- .../Models/OpenApiParameter.cs | 13 ++--- .../Models/OpenApiRequestBody.cs | 16 +++--- .../Models/OpenApiResponse.cs | 21 ++++---- .../References/OpenApiHeaderReference.cs | 2 +- .../References/OpenApiParameterReference.cs | 2 +- .../References/OpenApiRequestBodyReference.cs | 4 +- .../References/OpenApiResponseReference.cs | 2 +- .../Reader/ParseNodes/MapNode.cs | 32 ++++++++++- .../Reader/ParseNodes/ParseNode.cs | 5 ++ .../Reader/V2/OpenApiOperationDeserializer.cs | 5 +- .../Reader/V2/OpenApiResponseDeserializer.cs | 28 +++++----- .../Reader/V3/OpenApiParameterDeserializer.cs | 2 +- .../V3/OpenApiRequestBodyDeserializer.cs | 2 +- .../Reader/V3/OpenApiResponseDeserializer.cs | 2 +- .../V31/OpenApiParameterDeserializer.cs | 2 +- .../V31/OpenApiRequestBodyDeserializer.cs | 2 +- .../Reader/V31/OpenApiResponseDeserializer.cs | 2 +- .../Services/OpenApiVisitorBase.cs | 2 +- .../Services/OpenApiWalker.cs | 4 +- .../Validations/OpenApiValidator.cs | 2 +- .../Writers/OpenApiWriterExtensions.cs | 53 +++++++++++++++++++ .../Formatters/PowerShellFormatterTests.cs | 2 +- .../UtilityFiles/OpenApiDocumentMock.cs | 26 ++++----- .../PublicApi/PublicApi.approved.txt | 30 ++++++----- 29 files changed, 187 insertions(+), 95 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiHeader.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiHeader.cs index c6550caa6..a9febfb66 100644 --- a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiHeader.cs +++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiHeader.cs @@ -60,6 +60,6 @@ public interface IOpenApiHeader : IOpenApiDescribedElement, IOpenApiReadOnlyExte /// /// A map containing the representations for the header. /// - public IDictionary? Content { get; } + public IDictionary? Content { get; } } diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiParameter.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiParameter.cs index 63c3a860f..1148d9e9e 100644 --- a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiParameter.cs +++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiParameter.cs @@ -102,5 +102,5 @@ public interface IOpenApiParameter : IOpenApiDescribedElement, IOpenApiReadOnlyE /// When example or examples are provided in conjunction with the schema object, /// the example MUST follow the prescribed serialization strategy for the parameter. /// - public IDictionary? Content { get; } + public IDictionary? Content { get; } } diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiRequestBody.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiRequestBody.cs index b9c8304df..f38554a5e 100644 --- a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiRequestBody.cs +++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiRequestBody.cs @@ -19,7 +19,7 @@ public interface IOpenApiRequestBody : IOpenApiDescribedElement, IOpenApiReadOnl /// REQUIRED. The content of the request body. The key is a media type or media type range and the value describes it. /// For requests that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text/* /// - public IDictionary? Content { get; } + public IDictionary? Content { get; } /// /// Converts the request body to a body parameter in preparation for a v2 serialization. /// diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiResponse.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiResponse.cs index 379526e0a..d3a359082 100644 --- a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiResponse.cs +++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiResponse.cs @@ -18,7 +18,7 @@ public interface IOpenApiResponse : IOpenApiDescribedElement, IOpenApiReadOnlyEx /// A map containing descriptions of potential response payloads. /// The key is a media type or media type range and the value describes it. /// - public IDictionary? Content { get; } + public IDictionary? Content { get; } /// /// A map of operations links that can be followed from the response. diff --git a/src/Microsoft.OpenApi/Models/OpenApiHeader.cs b/src/Microsoft.OpenApi/Models/OpenApiHeader.cs index 82d17aece..8153337e1 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiHeader.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiHeader.cs @@ -50,7 +50,7 @@ public class OpenApiHeader : IOpenApiHeader, IOpenApiExtensible public IDictionary? Examples { get; set; } = new Dictionary(); /// - public IDictionary? Content { get; set; } = new Dictionary(); + public IDictionary? Content { get; set; } = new Dictionary(); /// public IDictionary? Extensions { get; set; } = new Dictionary(); @@ -76,7 +76,7 @@ internal OpenApiHeader(IOpenApiHeader header) Schema = header.Schema?.CreateShallowCopy(); Example = header.Example != null ? JsonNodeCloneHelper.Clone(header.Example) : null; Examples = header.Examples != null ? new Dictionary(header.Examples) : null; - Content = header.Content != null ? new Dictionary(header.Content) : null; + Content = header.Content != null ? new Dictionary(header.Content) : null; Extensions = header.Extensions != null ? new Dictionary(header.Extensions) : null; } @@ -85,7 +85,7 @@ internal OpenApiHeader(IOpenApiHeader header) /// public void SerializeAsV31(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV31(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV31(writer), (writer, element) => element?.SerializeAsV31(writer)); } /// @@ -93,11 +93,12 @@ public void SerializeAsV31(IOpenApiWriter writer) /// public void SerializeAsV3(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer), (writer, element) => element?.SerializeAsV3(writer)); } internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, - Action callback) + Action callback, + Action callbackForOptionals) { Utils.CheckArgumentNull(writer); @@ -134,7 +135,7 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio writer.WriteOptionalMap(OpenApiConstants.Examples, Examples, callback); // content - writer.WriteOptionalMap(OpenApiConstants.Content, Content, callback); + writer.WriteOptionalMapOfOptionals(OpenApiConstants.Content, Content, callbackForOptionals); // extensions writer.WriteExtensions(Extensions, version); diff --git a/src/Microsoft.OpenApi/Models/OpenApiParameter.cs b/src/Microsoft.OpenApi/Models/OpenApiParameter.cs index 652e65b3d..65c97d158 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiParameter.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiParameter.cs @@ -67,7 +67,7 @@ public bool Explode public JsonNode? Example { get; set; } /// - public IDictionary? Content { get; set; } = new Dictionary(); + public IDictionary? Content { get; set; } = new Dictionary(); /// public IDictionary? Extensions { get; set; } = new Dictionary(); @@ -93,7 +93,7 @@ internal OpenApiParameter(IOpenApiParameter parameter) Schema = parameter.Schema?.CreateShallowCopy(); Examples = parameter.Examples != null ? new Dictionary(parameter.Examples) : null; Example = parameter.Example != null ? JsonNodeCloneHelper.Clone(parameter.Example) : null; - Content = parameter.Content != null ? new Dictionary(parameter.Content) : null; + Content = parameter.Content != null ? new Dictionary(parameter.Content) : null; Extensions = parameter.Extensions != null ? new Dictionary(parameter.Extensions) : null; AllowEmptyValue = parameter.AllowEmptyValue; Deprecated = parameter.Deprecated; @@ -102,17 +102,18 @@ internal OpenApiParameter(IOpenApiParameter parameter) /// public void SerializeAsV31(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => element.SerializeAsV31(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => element.SerializeAsV31(writer), (writer, element) => element?.SerializeAsV31(writer)); } /// public void SerializeAsV3(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer), (writer, element) => element?.SerializeAsV3(writer)); } internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, - Action callback) + Action callback, + Action callbackForOptionals) { Utils.CheckArgumentNull(writer); @@ -158,7 +159,7 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio writer.WriteOptionalMap(OpenApiConstants.Examples, Examples, callback); // content - writer.WriteOptionalMap(OpenApiConstants.Content, Content, callback); + writer.WriteOptionalMapOfOptionals(OpenApiConstants.Content, Content, callbackForOptionals); // extensions writer.WriteExtensions(Extensions, version); diff --git a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs index 70d1a1309..87d8ace30 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs @@ -25,7 +25,7 @@ public class OpenApiRequestBody : IOpenApiReferenceable, IOpenApiExtensible, IOp public bool Required { get; set; } /// - public IDictionary? Content { get; set; } = new Dictionary(); + public IDictionary? Content { get; set; } = new Dictionary(); /// public IDictionary? Extensions { get; set; } = new Dictionary(); @@ -43,7 +43,7 @@ internal OpenApiRequestBody(IOpenApiRequestBody requestBody) Utils.CheckArgumentNull(requestBody); Description = requestBody.Description ?? Description; Required = requestBody.Required; - Content = requestBody.Content != null ? new Dictionary(requestBody.Content) : null; + Content = requestBody.Content != null ? new Dictionary(requestBody.Content) : null; Extensions = requestBody.Extensions != null ? new Dictionary(requestBody.Extensions) : null; } @@ -52,7 +52,7 @@ internal OpenApiRequestBody(IOpenApiRequestBody requestBody) /// public void SerializeAsV31(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => element.SerializeAsV31(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => element?.SerializeAsV31(writer)); } /// @@ -60,11 +60,11 @@ public void SerializeAsV31(IOpenApiWriter writer) /// public void SerializeAsV3(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element?.SerializeAsV3(writer)); } internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, - Action callback) + Action callbackForOptionals) { Utils.CheckArgumentNull(writer); @@ -74,7 +74,7 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio writer.WriteProperty(OpenApiConstants.Description, Description); // content - writer.WriteRequiredMap(OpenApiConstants.Content, Content, callback); + writer.WriteOptionalMapOfOptionals(OpenApiConstants.Content, Content, callbackForOptionals); // required writer.WriteProperty(OpenApiConstants.Required, Required, false); @@ -123,7 +123,7 @@ public IEnumerable ConvertToFormDataParameters(IOpenApiWriter { if (Content == null || !Content.Any()) yield break; - var properties = Content.First().Value.Schema?.Properties; + var properties = Content.First().Value?.Schema?.Properties; if(properties != null) { foreach (var property in properties) @@ -154,7 +154,7 @@ public IEnumerable ConvertToFormDataParameters(IOpenApiWriter Name = property.Key, Schema = paramSchema, Examples = Content.Values.FirstOrDefault()?.Examples, - Required = Content.First().Value.Schema?.Required?.Contains(property.Key) ?? false + Required = Content.First().Value?.Schema?.Required?.Contains(property.Key) ?? false }; } } diff --git a/src/Microsoft.OpenApi/Models/OpenApiResponse.cs b/src/Microsoft.OpenApi/Models/OpenApiResponse.cs index 9c8459e2b..2062ce2ef 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiResponse.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiResponse.cs @@ -22,7 +22,7 @@ public class OpenApiResponse : IOpenApiReferenceable, IOpenApiExtensible, IOpenA public IDictionary? Headers { get; set; } = new Dictionary(); /// - public IDictionary? Content { get; set; } = new Dictionary(); + public IDictionary? Content { get; set; } = new Dictionary(); /// public IDictionary? Links { get; set; } = new Dictionary(); @@ -43,7 +43,7 @@ internal OpenApiResponse(IOpenApiResponse response) Utils.CheckArgumentNull(response); Description = response.Description ?? Description; Headers = response.Headers != null ? new Dictionary(response.Headers) : null; - Content = response.Content != null ? new Dictionary(response.Content) : null; + Content = response.Content != null ? new Dictionary(response.Content) : null; Links = response.Links != null ? new Dictionary(response.Links) : null; Extensions = response.Extensions != null ? new Dictionary(response.Extensions) : null; } @@ -53,7 +53,7 @@ internal OpenApiResponse(IOpenApiResponse response) /// public void SerializeAsV31(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => element.SerializeAsV31(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => element.SerializeAsV31(writer), (writer, element) => element?.SerializeAsV31(writer)); } /// @@ -61,11 +61,12 @@ public void SerializeAsV31(IOpenApiWriter writer) /// public void SerializeAsV3(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer), (writer, element) => element?.SerializeAsV3(writer)); } private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, - Action callback) + Action callback, + Action callbackForOptionals) { Utils.CheckArgumentNull(writer); @@ -78,7 +79,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version writer.WriteOptionalMap(OpenApiConstants.Headers, Headers, callback); // content - writer.WriteOptionalMap(OpenApiConstants.Content, Content, callback); + writer.WriteOptionalMapOfOptionals(OpenApiConstants.Content, Content, callbackForOptionals); // links writer.WriteOptionalMap(OpenApiConstants.Links, Links, callback); @@ -112,14 +113,14 @@ public void SerializeAsV2(IOpenApiWriter writer) writer.WriteOptionalObject(OpenApiConstants.Schema, mediatype.Value.Schema, (w, s) => s.SerializeAsV2(w)); // examples - if (Content.Values.Any(m => m.Example != null)) + if (Content.Values.Any(m => m?.Example != null)) { writer.WritePropertyName(OpenApiConstants.Examples); writer.WriteStartObject(); foreach (var mediaTypePair in Content) { - if (mediaTypePair.Value.Example != null) + if (mediaTypePair.Value?.Example != null) { writer.WritePropertyName(mediaTypePair.Key); writer.WriteAny(mediaTypePair.Value.Example); @@ -129,13 +130,13 @@ public void SerializeAsV2(IOpenApiWriter writer) writer.WriteEndObject(); } - if (Content.Values.Any(m => m.Examples != null && m.Examples.Any())) + if (Content.Values.Any(m => m?.Examples != null && m.Examples.Any())) { writer.WritePropertyName(OpenApiConstants.ExamplesExtension); writer.WriteStartObject(); foreach (var example in Content - .Select(static x => x.Value.Examples) + .Select(static x => x.Value?.Examples) .OfType>() .SelectMany(static x => x)) { diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiHeaderReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiHeaderReference.cs index cd843de57..95f747f63 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiHeaderReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiHeaderReference.cs @@ -71,7 +71,7 @@ public string? Description public IDictionary? Examples { get => Target?.Examples; } /// - public IDictionary? Content { get => Target?.Content; } + public IDictionary? Content { get => Target?.Content; } /// public IDictionary? Extensions { get => Target?.Extensions; } diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiParameterReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiParameterReference.cs index 40ecb69eb..283d29d9e 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiParameterReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiParameterReference.cs @@ -76,7 +76,7 @@ public string? Description public bool Explode { get => Target?.Explode ?? default; } /// - public IDictionary? Content { get => Target?.Content; } + public IDictionary? Content { get => Target?.Content; } /// public IDictionary? Extensions { get => Target?.Extensions; } diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiRequestBodyReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiRequestBodyReference.cs index 8beb3e604..e9770f170 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiRequestBodyReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiRequestBodyReference.cs @@ -45,7 +45,7 @@ public string? Description } /// - public IDictionary? Content { get => Target?.Content; } + public IDictionary? Content { get => Target?.Content; } /// public bool Required { get => Target?.Required ?? false; } @@ -84,7 +84,7 @@ public override void SerializeAsV2(IOpenApiWriter writer) if (Content == null || !Content.Any()) return []; - return Content.First().Value.Schema?.Properties?.Select(x => new OpenApiParameterReference(x.Key, Reference.HostDocument)); + return Content.First().Value?.Schema?.Properties?.Select(x => new OpenApiParameterReference(x.Key, Reference.HostDocument)); } /// diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiResponseReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiResponseReference.cs index cc78f6080..5dbf3c0d5 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiResponseReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiResponseReference.cs @@ -43,7 +43,7 @@ public string? Description } /// - public IDictionary? Content { get => Target?.Content; } + public IDictionary? Content { get => Target?.Content; } /// public IDictionary? Headers { get => Target?.Headers; } diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index 3d3ef71af..9b154ccaf 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -62,7 +62,37 @@ public override Dictionary CreateMap(Func k.key, v => v.value); + } + + public override Dictionary CreateMapOfOptionals(Func map, OpenApiDocument hostDocument) where T : class + { + var jsonMap = _node ?? throw new OpenApiReaderException($"Expected map while parsing {typeof(T).Name}", Context); + var nodes = jsonMap.Select( + n => + { + + var key = n.Key; + T? value; + try + { + Context.StartObject(key); + value = n.Value is JsonObject jsonObject + ? map(new MapNode(Context, jsonObject), hostDocument) + : default; } finally { diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/ParseNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/ParseNode.cs index 699022193..fab2d8421 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/ParseNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/ParseNode.cs @@ -56,6 +56,11 @@ public virtual Dictionary CreateMap(Func CreateMapOfOptionals(Func map, OpenApiDocument hostDocument) where T : class + { + throw new OpenApiReaderException("Cannot create map from this type of node.", Context); + } + public virtual List CreateSimpleList(Func map, OpenApiDocument openApiDocument) { throw new OpenApiReaderException("Cannot create simple list from this type of node.", Context); diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs index 4bd28a18d..066724f10 100644 --- a/src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs @@ -201,10 +201,11 @@ private static OpenApiRequestBody CreateFormBody(ParsingContext context, List k, - _ => mediaType) + _ => (OpenApiMediaType?)mediaType) }; foreach (var value in formBody.Content.Values + .OfType() .Where(static x => x.Schema is not null && x.Schema.Properties is not null && x.Schema.Properties.Any() @@ -230,7 +231,7 @@ internal static IOpenApiRequestBody CreateRequestBody( Required = bodyParameter.Required, Content = consumes.ToDictionary( k => k, - _ => new OpenApiMediaType + _ => (OpenApiMediaType?)new OpenApiMediaType { Schema = bodyParameter.Schema, Examples = bodyParameter.Examples diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiResponseDeserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiResponseDeserializer.cs index 653208578..e68313271 100644 --- a/src/Microsoft.OpenApi/Reader/V2/OpenApiResponseDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiResponseDeserializer.cs @@ -46,15 +46,15 @@ internal static partial class OpenApiV2Deserializer (o, p, n, _) => o.AddExtension(p, LoadExtension(p, n))} }; - private static readonly AnyFieldMap _mediaTypeAnyFields = + private static readonly AnyFieldMap _mediaTypeAnyFields = new() { { OpenApiConstants.Example, new( - m => m.Example, - (m, v) => m.Example = v, - m => m.Schema) + m => m?.Example, + (m, v) => {if (m is not null) { m.Example = v; }}, + m => m?.Schema) } }; @@ -62,7 +62,7 @@ private static void ProcessProduces(MapNode mapNode, OpenApiResponse response, P { if (response.Content == null) { - response.Content = new Dictionary(); + response.Content = new Dictionary(); } else if (context.GetFromTempStorage(TempStorageKeys.ResponseProducesSet, response)) { @@ -80,13 +80,10 @@ private static void ProcessProduces(MapNode mapNode, OpenApiResponse response, P foreach (var produce in produces) { - if (response.Content.TryGetValue(produce, out var produceValue)) + if (response.Content.TryGetValue(produce, out var produceValue) && schema is not null && produceValue is not null) { - if (schema != null) - { - produceValue.Schema = schema; - ProcessAnyFields(mapNode, produceValue, _mediaTypeAnyFields); - } + produceValue.Schema = schema; + ProcessAnyFields(mapNode, produceValue, _mediaTypeAnyFields); } else { @@ -160,9 +157,9 @@ private static void LoadExample(OpenApiResponse response, string mediaType, Pars { var exampleNode = node.CreateAny(); - response.Content ??= new Dictionary(); + response.Content ??= new Dictionary(); - OpenApiMediaType mediaTypeObject; + OpenApiMediaType? mediaTypeObject; if (response.Content.TryGetValue(mediaType, out var value)) { mediaTypeObject = value; @@ -176,7 +173,8 @@ private static void LoadExample(OpenApiResponse response, string mediaType, Pars response.Content.Add(mediaType, mediaTypeObject); } - mediaTypeObject.Example = exampleNode; + if (mediaTypeObject is not null) + mediaTypeObject.Example = exampleNode; } public static IOpenApiResponse LoadResponse(ParseNode node, OpenApiDocument hostDocument) @@ -200,7 +198,7 @@ public static IOpenApiResponse LoadResponse(ParseNode node, OpenApiDocument host { foreach (var mediaType in response.Content.Values) { - if (mediaType.Schema != null) + if (mediaType?.Schema != null) { ProcessAnyFields(mapNode, mediaType, _mediaTypeAnyFields); } diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiParameterDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiParameterDeserializer.cs index 33e67b953..a85e3bfe9 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiParameterDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiParameterDeserializer.cs @@ -109,7 +109,7 @@ internal static partial class OpenApiV3Deserializer }, { "content", - (o, n, t) => o.Content = n.CreateMap(LoadMediaType, t) + (o, n, t) => o.Content = n.CreateMapOfOptionals(LoadMediaType, t) }, { "examples", diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiRequestBodyDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiRequestBodyDeserializer.cs index 339ca437e..b7933dc9b 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiRequestBodyDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiRequestBodyDeserializer.cs @@ -25,7 +25,7 @@ internal static partial class OpenApiV3Deserializer }, { "content", - (o, n, t) => o.Content = n.CreateMap(LoadMediaType, t) + (o, n, t) => o.Content = n.CreateMapOfOptionals(LoadMediaType, t) }, { "required", diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiResponseDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiResponseDeserializer.cs index 8e10c2e79..c9263eaf2 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiResponseDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiResponseDeserializer.cs @@ -28,7 +28,7 @@ internal static partial class OpenApiV3Deserializer }, { "content", - (o, n, t) => o.Content = n.CreateMap(LoadMediaType, t) + (o, n, t) => o.Content = n.CreateMapOfOptionals(LoadMediaType, t) }, { "links", diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiParameterDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiParameterDeserializer.cs index eae4e4993..5580982cc 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiParameterDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiParameterDeserializer.cs @@ -111,7 +111,7 @@ internal static partial class OpenApiV31Deserializer { "content", (o, n, t) => { - o.Content = n.CreateMap(LoadMediaType, t); + o.Content = n.CreateMapOfOptionals(LoadMediaType, t); } }, { diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiRequestBodyDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiRequestBodyDeserializer.cs index ef08b1b2b..d6f653133 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiRequestBodyDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiRequestBodyDeserializer.cs @@ -25,7 +25,7 @@ internal static partial class OpenApiV31Deserializer { "content", (o, n, t) => { - o.Content = n.CreateMap(LoadMediaType, t); + o.Content = n.CreateMapOfOptionals(LoadMediaType, t); } }, { diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiResponseDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiResponseDeserializer.cs index 6d910761c..907a46b92 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiResponseDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiResponseDeserializer.cs @@ -30,7 +30,7 @@ internal static partial class OpenApiV31Deserializer { "content", (o, n, t) => { - o.Content = n.CreateMap(LoadMediaType, t); + o.Content = n.CreateMapOfOptionals(LoadMediaType, t); } }, { diff --git a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs index aae8527d7..87aec2923 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs @@ -191,7 +191,7 @@ public virtual void Visit(OpenApiResponses response) /// /// Visits media type content. /// - public virtual void Visit(IDictionary content) + public virtual void Visit(IDictionary content) { } diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index cf07356d3..866292da3 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -794,7 +794,7 @@ internal void Walk(IDictionary? callbacks) /// /// Visits dictionary of /// - internal void Walk(IDictionary? content) + internal void Walk(IDictionary? content) { if (content == null) { @@ -816,7 +816,7 @@ internal void Walk(IDictionary? content) /// /// Visits and child objects /// - internal void Walk(OpenApiMediaType mediaType) + internal void Walk(OpenApiMediaType? mediaType) { if (mediaType == null) { diff --git a/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs b/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs index 54bd92deb..a94aa93fa 100644 --- a/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs +++ b/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs @@ -155,7 +155,7 @@ public void AddWarning(OpenApiValidatorWarning warning) /// public override void Visit(IDictionary callbacks) => Validate(callbacks, callbacks.GetType()); /// - public override void Visit(IDictionary content) => Validate(content, content.GetType()); + public override void Visit(IDictionary content) => Validate(content, content.GetType()); /// public override void Visit(IDictionary examples) => Validate(examples, examples.GetType()); /// diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs index ad84d5537..0961b28ae 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs @@ -351,6 +351,27 @@ public static void WriteOptionalMap( } } + /// + /// Write the optional Open API element map. + /// + /// The Open API element type. + /// The Open API writer. + /// The property name. + /// The map values. + /// The map element writer action with writer and value as input. + public static void WriteOptionalMapOfOptionals( + this IOpenApiWriter writer, + string name, + IDictionary? elements, + Action action) + where T : IOpenApiElement + { + if (elements != null && elements.Any()) + { + writer.WriteMapOfOptionalsInternal(name, elements, action); + } + } + /// /// Write the optional Open API element map. /// @@ -427,6 +448,15 @@ private static void WriteMapInternal( WriteMapInternal(writer, name, elements, (w, _, s) => action(w, s)); } + private static void WriteMapOfOptionalsInternal( + this IOpenApiWriter writer, + string name, + IDictionary? elements, + Action action) + { + WriteMapOfOptionalsInternal(writer, name, elements, (w, _, s) => action(w, s)); + } + private static void WriteMapInternal( this IOpenApiWriter writer, string name, @@ -456,5 +486,28 @@ private static void WriteMapInternal( writer.WriteEndObject(); } + + private static void WriteMapOfOptionalsInternal( + this IOpenApiWriter writer, + string name, + IDictionary? elements, + Action action) + { + Utils.CheckArgumentNull(action); + + writer.WritePropertyName(name); + writer.WriteStartObject(); + + if (elements != null) + { + foreach (var item in elements) + { + writer.WritePropertyName(item.Key); + action(writer, item.Key, item.Value); + } + } + + writer.WriteEndObject(); + } } } diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Formatters/PowerShellFormatterTests.cs b/test/Microsoft.OpenApi.Hidi.Tests/Formatters/PowerShellFormatterTests.cs index 92cfae013..7b6532416 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Formatters/PowerShellFormatterTests.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/Formatters/PowerShellFormatterTests.cs @@ -125,7 +125,7 @@ private static OpenApiDocument GetSampleOpenApiDocument() { Name = "ids", In = ParameterLocation.Query, - Content = new Dictionary + Content = new Dictionary { { "application/json", diff --git a/test/Microsoft.OpenApi.Hidi.Tests/UtilityFiles/OpenApiDocumentMock.cs b/test/Microsoft.OpenApi.Hidi.Tests/UtilityFiles/OpenApiDocumentMock.cs index 71768bfbe..f9228d354 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/UtilityFiles/OpenApiDocumentMock.cs +++ b/test/Microsoft.OpenApi.Hidi.Tests/UtilityFiles/OpenApiDocumentMock.cs @@ -100,7 +100,7 @@ public static OpenApiDocument CreateOpenApiDocument() "200", new OpenApiResponse() { Description = "Success", - Content = new Dictionary + Content = new Dictionary { { applicationJsonMediaType, @@ -165,7 +165,7 @@ public static OpenApiDocument CreateOpenApiDocument() "200", new OpenApiResponse() { Description = "Success", - Content = new Dictionary + Content = new Dictionary { { applicationJsonMediaType, @@ -213,7 +213,7 @@ public static OpenApiDocument CreateOpenApiDocument() "200", new OpenApiResponse() { Description = "Retrieved entities", - Content = new Dictionary + Content = new Dictionary { { applicationJsonMediaType, @@ -259,7 +259,7 @@ public static OpenApiDocument CreateOpenApiDocument() "200", new OpenApiResponse() { Description = "Retrieved entity", - Content = new Dictionary + Content = new Dictionary { { applicationJsonMediaType, @@ -322,7 +322,7 @@ public static OpenApiDocument CreateOpenApiDocument() "200", new OpenApiResponse() { Description = "Retrieved navigation property", - Content = new Dictionary + Content = new Dictionary { { applicationJsonMediaType, @@ -369,7 +369,7 @@ public static OpenApiDocument CreateOpenApiDocument() "200", new OpenApiResponse() { Description = "Success", - Content = new Dictionary + Content = new Dictionary { { applicationJsonMediaType, @@ -426,7 +426,7 @@ public static OpenApiDocument CreateOpenApiDocument() "200", new OpenApiResponse() { Description = "Retrieved navigation property", - Content = new Dictionary + Content = new Dictionary { { applicationJsonMediaType, @@ -557,7 +557,7 @@ public static OpenApiDocument CreateOpenApiDocument() "200", new OpenApiResponse() { Description = "Success", - Content = new Dictionary + Content = new Dictionary { { applicationJsonMediaType, @@ -690,11 +690,11 @@ public static OpenApiDocument CreateOpenApiDocument() document.Paths[communicationsCallsKeepAlivePath].Operations![HttpMethod.Post].Tags = new HashSet {new OpenApiTagReference("communications.Actions", document)}; document.Paths[eventsDeltaPath].Operations![HttpMethod.Get].Tags = new HashSet {new OpenApiTagReference("groups.Functions", document)}; document.Paths[refPath].Operations![HttpMethod.Get].Tags = new HashSet {new OpenApiTagReference("applications.directoryObject", document)}; - ((OpenApiSchema)document.Paths[usersPath].Operations![HttpMethod.Get].Responses!["200"].Content![applicationJsonMediaType].Schema!.Properties!["value"]).Items = new OpenApiSchemaReference("microsoft.graph.user", document); - document.Paths[usersByIdPath].Operations![HttpMethod.Get].Responses!["200"].Content![applicationJsonMediaType].Schema = new OpenApiSchemaReference("microsoft.graph.user", document); - document.Paths[messagesByIdPath].Operations![HttpMethod.Get].Responses!["200"].Content![applicationJsonMediaType].Schema = new OpenApiSchemaReference("microsoft.graph.message", document); - ((OpenApiSchema)document.Paths[securityProfilesPath].Operations![HttpMethod.Get].Responses!["200"].Content![applicationJsonMediaType].Schema!.Properties!["value"]).Items = new OpenApiSchemaReference("microsoft.graph.networkInterface", document); - ((OpenApiSchema)document.Paths[eventsDeltaPath].Operations![HttpMethod.Get].Responses!["200"].Content![applicationJsonMediaType].Schema!.Properties!["value"]).Items = new OpenApiSchemaReference("microsoft.graph.event", document); + ((OpenApiSchema)document.Paths[usersPath].Operations![HttpMethod.Get].Responses!["200"].Content![applicationJsonMediaType]!.Schema!.Properties!["value"]).Items = new OpenApiSchemaReference("microsoft.graph.user", document); + document.Paths[usersByIdPath].Operations![HttpMethod.Get].Responses!["200"].Content![applicationJsonMediaType]!.Schema = new OpenApiSchemaReference("microsoft.graph.user", document); + document.Paths[messagesByIdPath].Operations![HttpMethod.Get].Responses!["200"].Content![applicationJsonMediaType]!.Schema = new OpenApiSchemaReference("microsoft.graph.message", document); + ((OpenApiSchema)document.Paths[securityProfilesPath].Operations![HttpMethod.Get].Responses!["200"].Content![applicationJsonMediaType]!.Schema!.Properties!["value"]).Items = new OpenApiSchemaReference("microsoft.graph.networkInterface", document); + ((OpenApiSchema)document.Paths[eventsDeltaPath].Operations![HttpMethod.Get].Responses!["200"].Content![applicationJsonMediaType]!.Schema!.Properties!["value"]).Items = new OpenApiSchemaReference("microsoft.graph.event", document); return document; } } diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 0d691d05e..95136fd5e 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -355,7 +355,7 @@ namespace Microsoft.OpenApi.Models.Interfaces { bool AllowEmptyValue { get; } bool AllowReserved { get; } - System.Collections.Generic.IDictionary? Content { get; } + System.Collections.Generic.IDictionary? Content { get; } bool Deprecated { get; } System.Text.Json.Nodes.JsonNode? Example { get; } System.Collections.Generic.IDictionary? Examples { get; } @@ -376,7 +376,7 @@ namespace Microsoft.OpenApi.Models.Interfaces { bool AllowEmptyValue { get; } bool AllowReserved { get; } - System.Collections.Generic.IDictionary? Content { get; } + System.Collections.Generic.IDictionary? Content { get; } bool Deprecated { get; } System.Text.Json.Nodes.JsonNode? Example { get; } System.Collections.Generic.IDictionary? Examples { get; } @@ -399,14 +399,14 @@ namespace Microsoft.OpenApi.Models.Interfaces } public interface IOpenApiRequestBody : Microsoft.OpenApi.Interfaces.IOpenApiElement, Microsoft.OpenApi.Interfaces.IOpenApiReadOnlyExtensible, Microsoft.OpenApi.Interfaces.IOpenApiReferenceable, Microsoft.OpenApi.Interfaces.IOpenApiSerializable, Microsoft.OpenApi.Interfaces.IShallowCopyable, Microsoft.OpenApi.Models.Interfaces.IOpenApiDescribedElement { - System.Collections.Generic.IDictionary? Content { get; } + System.Collections.Generic.IDictionary? Content { get; } bool Required { get; } Microsoft.OpenApi.Models.Interfaces.IOpenApiParameter? ConvertToBodyParameter(Microsoft.OpenApi.Writers.IOpenApiWriter writer); System.Collections.Generic.IEnumerable? ConvertToFormDataParameters(Microsoft.OpenApi.Writers.IOpenApiWriter writer); } public interface IOpenApiResponse : Microsoft.OpenApi.Interfaces.IOpenApiElement, Microsoft.OpenApi.Interfaces.IOpenApiReadOnlyExtensible, Microsoft.OpenApi.Interfaces.IOpenApiReferenceable, Microsoft.OpenApi.Interfaces.IOpenApiSerializable, Microsoft.OpenApi.Interfaces.IShallowCopyable, Microsoft.OpenApi.Models.Interfaces.IOpenApiDescribedElement { - System.Collections.Generic.IDictionary? Content { get; } + System.Collections.Generic.IDictionary? Content { get; } System.Collections.Generic.IDictionary? Headers { get; } System.Collections.Generic.IDictionary? Links { get; } } @@ -797,7 +797,7 @@ namespace Microsoft.OpenApi.Models public OpenApiHeader() { } public bool AllowEmptyValue { get; set; } public bool AllowReserved { get; set; } - public System.Collections.Generic.IDictionary? Content { get; set; } + public System.Collections.Generic.IDictionary? Content { get; set; } public bool Deprecated { get; set; } public string? Description { get; set; } public System.Text.Json.Nodes.JsonNode? Example { get; set; } @@ -922,7 +922,7 @@ namespace Microsoft.OpenApi.Models public OpenApiParameter() { } public bool AllowEmptyValue { get; set; } public bool AllowReserved { get; set; } - public System.Collections.Generic.IDictionary? Content { get; set; } + public System.Collections.Generic.IDictionary? Content { get; set; } public bool Deprecated { get; set; } public string? Description { get; set; } public System.Text.Json.Nodes.JsonNode? Example { get; set; } @@ -981,7 +981,7 @@ namespace Microsoft.OpenApi.Models public class OpenApiRequestBody : Microsoft.OpenApi.Interfaces.IOpenApiElement, Microsoft.OpenApi.Interfaces.IOpenApiExtensible, Microsoft.OpenApi.Interfaces.IOpenApiReadOnlyExtensible, Microsoft.OpenApi.Interfaces.IOpenApiReferenceable, Microsoft.OpenApi.Interfaces.IOpenApiSerializable, Microsoft.OpenApi.Interfaces.IShallowCopyable, Microsoft.OpenApi.Models.Interfaces.IOpenApiDescribedElement, Microsoft.OpenApi.Models.Interfaces.IOpenApiRequestBody { public OpenApiRequestBody() { } - public System.Collections.Generic.IDictionary? Content { get; set; } + public System.Collections.Generic.IDictionary? Content { get; set; } public string? Description { get; set; } public System.Collections.Generic.IDictionary? Extensions { get; set; } public bool Required { get; set; } @@ -995,7 +995,7 @@ namespace Microsoft.OpenApi.Models public class OpenApiResponse : Microsoft.OpenApi.Interfaces.IOpenApiElement, Microsoft.OpenApi.Interfaces.IOpenApiExtensible, Microsoft.OpenApi.Interfaces.IOpenApiReadOnlyExtensible, Microsoft.OpenApi.Interfaces.IOpenApiReferenceable, Microsoft.OpenApi.Interfaces.IOpenApiSerializable, Microsoft.OpenApi.Interfaces.IShallowCopyable, Microsoft.OpenApi.Models.Interfaces.IOpenApiDescribedElement, Microsoft.OpenApi.Models.Interfaces.IOpenApiResponse { public OpenApiResponse() { } - public System.Collections.Generic.IDictionary? Content { get; set; } + public System.Collections.Generic.IDictionary? Content { get; set; } public string? Description { get; set; } public System.Collections.Generic.IDictionary? Extensions { get; set; } public System.Collections.Generic.IDictionary? Headers { get; set; } @@ -1258,7 +1258,7 @@ namespace Microsoft.OpenApi.Models.References public OpenApiHeaderReference(string referenceId, Microsoft.OpenApi.Models.OpenApiDocument? hostDocument = null, string? externalResource = null) { } public bool AllowEmptyValue { get; } public bool AllowReserved { get; } - public System.Collections.Generic.IDictionary? Content { get; } + public System.Collections.Generic.IDictionary? Content { get; } public bool Deprecated { get; } public string? Description { get; set; } public System.Text.Json.Nodes.JsonNode? Example { get; } @@ -1290,7 +1290,7 @@ namespace Microsoft.OpenApi.Models.References public OpenApiParameterReference(string referenceId, Microsoft.OpenApi.Models.OpenApiDocument? hostDocument = null, string? externalResource = null) { } public bool AllowEmptyValue { get; } public bool AllowReserved { get; } - public System.Collections.Generic.IDictionary? Content { get; } + public System.Collections.Generic.IDictionary? Content { get; } public bool Deprecated { get; } public string? Description { get; set; } public System.Text.Json.Nodes.JsonNode? Example { get; } @@ -1321,7 +1321,7 @@ namespace Microsoft.OpenApi.Models.References public class OpenApiRequestBodyReference : Microsoft.OpenApi.Models.References.BaseOpenApiReferenceHolder, Microsoft.OpenApi.Interfaces.IOpenApiElement, Microsoft.OpenApi.Interfaces.IOpenApiReadOnlyExtensible, Microsoft.OpenApi.Interfaces.IOpenApiReferenceable, Microsoft.OpenApi.Interfaces.IOpenApiSerializable, Microsoft.OpenApi.Interfaces.IShallowCopyable, Microsoft.OpenApi.Models.Interfaces.IOpenApiDescribedElement, Microsoft.OpenApi.Models.Interfaces.IOpenApiRequestBody { public OpenApiRequestBodyReference(string referenceId, Microsoft.OpenApi.Models.OpenApiDocument? hostDocument = null, string? externalResource = null) { } - public System.Collections.Generic.IDictionary? Content { get; } + public System.Collections.Generic.IDictionary? Content { get; } public string? Description { get; set; } public System.Collections.Generic.IDictionary? Extensions { get; } public bool Required { get; } @@ -1334,7 +1334,7 @@ namespace Microsoft.OpenApi.Models.References public class OpenApiResponseReference : Microsoft.OpenApi.Models.References.BaseOpenApiReferenceHolder, Microsoft.OpenApi.Interfaces.IOpenApiElement, Microsoft.OpenApi.Interfaces.IOpenApiReadOnlyExtensible, Microsoft.OpenApi.Interfaces.IOpenApiReferenceable, Microsoft.OpenApi.Interfaces.IOpenApiSerializable, Microsoft.OpenApi.Interfaces.IShallowCopyable, Microsoft.OpenApi.Models.Interfaces.IOpenApiDescribedElement, Microsoft.OpenApi.Models.Interfaces.IOpenApiResponse { public OpenApiResponseReference(string referenceId, Microsoft.OpenApi.Models.OpenApiDocument? hostDocument = null, string? externalResource = null) { } - public System.Collections.Generic.IDictionary? Content { get; } + public System.Collections.Generic.IDictionary? Content { get; } public string? Description { get; set; } public System.Collections.Generic.IDictionary? Extensions { get; } public System.Collections.Generic.IDictionary? Headers { get; } @@ -1630,7 +1630,7 @@ namespace Microsoft.OpenApi.Services public virtual void Visit(System.Collections.Generic.IDictionary links) { } public virtual void Visit(System.Collections.Generic.IDictionary webhooks) { } public virtual void Visit(System.Collections.Generic.IDictionary encodings) { } - public virtual void Visit(System.Collections.Generic.IDictionary content) { } + public virtual void Visit(System.Collections.Generic.IDictionary content) { } public virtual void Visit(System.Collections.Generic.IDictionary serverVariables) { } public virtual void Visit(System.Collections.Generic.IList example) { } public virtual void Visit(System.Collections.Generic.IList parameters) { } @@ -1725,7 +1725,7 @@ namespace Microsoft.OpenApi.Validations public override void Visit(System.Collections.Generic.IDictionary headers) { } public override void Visit(System.Collections.Generic.IDictionary links) { } public override void Visit(System.Collections.Generic.IDictionary encodings) { } - public override void Visit(System.Collections.Generic.IDictionary content) { } + public override void Visit(System.Collections.Generic.IDictionary content) { } public override void Visit(System.Collections.Generic.IDictionary serverVariables) { } public override void Visit(System.Collections.Generic.IList example) { } } @@ -1976,6 +1976,8 @@ namespace Microsoft.OpenApi.Writers where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { } public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary? elements, System.Action action) where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { } + public static void WriteOptionalMapOfOptionals(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary? elements, System.Action action) + where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { } public static void WriteOptionalObject(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, T? value, System.Action action) { } public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, string? value) { } public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, bool value, bool defaultValue = false) { } From e1523e487e4f60428b90c5edbed33f96dbc83e99 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 28 Mar 2025 13:48:50 -0400 Subject: [PATCH 2/3] chore: adds test for the empty media type Signed-off-by: Vincent Biret --- ...Type_produceTerseOutput=False.verified.txt | 7 +++++ ...aType_produceTerseOutput=True.verified.txt | 1 + .../Models/OpenApiRequestBodyTests.cs | 27 +++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.SerializeRequestBodyWithNoMediaType_produceTerseOutput=False.verified.txt create mode 100644 test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.SerializeRequestBodyWithNoMediaType_produceTerseOutput=True.verified.txt diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.SerializeRequestBodyWithNoMediaType_produceTerseOutput=False.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.SerializeRequestBodyWithNoMediaType_produceTerseOutput=False.verified.txt new file mode 100644 index 000000000..e78246500 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.SerializeRequestBodyWithNoMediaType_produceTerseOutput=False.verified.txt @@ -0,0 +1,7 @@ +{ + "description": "description", + "content": { + "application/octet-stream": { } + }, + "required": true +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.SerializeRequestBodyWithNoMediaType_produceTerseOutput=True.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.SerializeRequestBodyWithNoMediaType_produceTerseOutput=True.verified.txt new file mode 100644 index 000000000..cf34c1cd2 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.SerializeRequestBodyWithNoMediaType_produceTerseOutput=True.verified.txt @@ -0,0 +1 @@ +{"description":"description","content":{"application/octet-stream":{}},"required":true} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.cs index 863ce5145..c715ea32a 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Threading.Tasks; @@ -98,5 +99,31 @@ public async Task SerializeReferencedRequestBodyAsV3JsonWithoutReferenceWorksAsy // Assert await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SerializeRequestBodyWithNoMediaType(bool produceTerseOutput) + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = produceTerseOutput }); + var requestBody = new OpenApiRequestBody() + { + Description = "description", + Required = true, + Content = new Dictionary() + { + {"application/octet-stream", null}, + } + }; + + // Act + requestBody.SerializeAsV3(writer); + await writer.FlushAsync(); + + // Assert + await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); + } } } From e905e406072f55a32cf098564dc7e6acb30a3155 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 28 Mar 2025 13:49:04 -0400 Subject: [PATCH 3/3] fix: serializes an empty object to be compliant with the specification Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/Models/OpenApiHeader.cs | 22 +++++++++++++++++-- .../Models/OpenApiParameter.cs | 22 +++++++++++++++++-- .../Models/OpenApiRequestBody.cs | 22 +++++++++++++++++-- .../Models/OpenApiResponse.cs | 22 +++++++++++++++++-- 4 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiHeader.cs b/src/Microsoft.OpenApi/Models/OpenApiHeader.cs index 8153337e1..15c49ea02 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiHeader.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiHeader.cs @@ -85,7 +85,16 @@ internal OpenApiHeader(IOpenApiHeader header) /// public void SerializeAsV31(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV31(writer), (writer, element) => element?.SerializeAsV31(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV31(writer), (writer, element) => + { + if(element is null) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + } + else + element.SerializeAsV31(writer); + }); } /// @@ -93,7 +102,16 @@ public void SerializeAsV31(IOpenApiWriter writer) /// public void SerializeAsV3(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer), (writer, element) => element?.SerializeAsV3(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer), (writer, element) => + { + if(element is null) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + } + else + element.SerializeAsV3(writer); + }); } internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, diff --git a/src/Microsoft.OpenApi/Models/OpenApiParameter.cs b/src/Microsoft.OpenApi/Models/OpenApiParameter.cs index 65c97d158..dbf656616 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiParameter.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiParameter.cs @@ -102,13 +102,31 @@ internal OpenApiParameter(IOpenApiParameter parameter) /// public void SerializeAsV31(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => element.SerializeAsV31(writer), (writer, element) => element?.SerializeAsV31(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => element.SerializeAsV31(writer), (writer, element) => + { + if(element is null) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + } + else + element.SerializeAsV31(writer); + }); } /// public void SerializeAsV3(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer), (writer, element) => element?.SerializeAsV3(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer), (writer, element) => + { + if(element is null) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + } + else + element.SerializeAsV3(writer); + }); } internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, diff --git a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs index 87d8ace30..35e045fee 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs @@ -52,7 +52,16 @@ internal OpenApiRequestBody(IOpenApiRequestBody requestBody) /// public void SerializeAsV31(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => element?.SerializeAsV31(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => + { + if(element is null) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + } + else + element.SerializeAsV31(writer); + }); } /// @@ -60,7 +69,16 @@ public void SerializeAsV31(IOpenApiWriter writer) /// public void SerializeAsV3(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element?.SerializeAsV3(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => + { + if(element is null) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + } + else + element.SerializeAsV3(writer); + }); } internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, diff --git a/src/Microsoft.OpenApi/Models/OpenApiResponse.cs b/src/Microsoft.OpenApi/Models/OpenApiResponse.cs index 2062ce2ef..9164a31f5 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiResponse.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiResponse.cs @@ -53,7 +53,16 @@ internal OpenApiResponse(IOpenApiResponse response) /// public void SerializeAsV31(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => element.SerializeAsV31(writer), (writer, element) => element?.SerializeAsV31(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => element.SerializeAsV31(writer), (writer, element) => + { + if(element is null) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + } + else + element.SerializeAsV31(writer); + }); } /// @@ -61,7 +70,16 @@ public void SerializeAsV31(IOpenApiWriter writer) /// public void SerializeAsV3(IOpenApiWriter writer) { - SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer), (writer, element) => element?.SerializeAsV3(writer)); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer), (writer, element) => + { + if(element is null) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + } + else + element.SerializeAsV3(writer); + }); } private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,