diff --git a/.github/workflows/release-please-gha.yml b/.github/workflows/release-please-gha.yml
index 28f1d59d1..a730548a2 100644
--- a/.github/workflows/release-please-gha.yml
+++ b/.github/workflows/release-please-gha.yml
@@ -14,6 +14,7 @@ on:
push:
branches:
- main
+ - support/v1
jobs:
release:
@@ -33,4 +34,4 @@ jobs:
with:
token: ${{ steps.app-token.outputs.token }}
config-file: release-please-config.json
- manifest-file: .release-please-manifest.json
\ No newline at end of file
+ manifest-file: .release-please-manifest.json
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 656a2ef17..bfc26f9c4 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "2.1.0"
+ ".": "2.2.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a877a5be..7c064f287 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## [2.2.0](https://github.com/microsoft/OpenAPI.NET/compare/v2.1.0...v2.2.0) (2025-08-25)
+
+
+### Features
+
+* add Validation Rule for path operations to not have a request body ([d101fc3](https://github.com/microsoft/OpenAPI.NET/commit/d101fc30cfc701f2d6c52a51b9e39fa7eae96194))
+
+
+### Bug Fixes
+
+* missing examples when one example is with an empty array. ([cb1c496](https://github.com/microsoft/OpenAPI.NET/commit/cb1c4967f37f11dad6ad42784e6c3cf8570081f9))
+
## [2.1.0](https://github.com/microsoft/OpenAPI.NET/compare/v2.0.1...v2.1.0) (2025-08-20)
diff --git a/Directory.Build.props b/Directory.Build.props
index 5b7324dd7..c7dadf189 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -12,7 +12,7 @@
https://github.com/Microsoft/OpenAPI.NET
© Microsoft Corporation. All rights reserved.
OpenAPI .NET
- 2.1.0
+ 2.2.0
diff --git a/src/Microsoft.OpenApi/Models/OpenApiExample.cs b/src/Microsoft.OpenApi/Models/OpenApiExample.cs
index 0ebea9da9..b23befd08 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiExample.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiExample.cs
@@ -70,7 +70,10 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
writer.WriteProperty(OpenApiConstants.Description, Description);
// value
- writer.WriteOptionalObject(OpenApiConstants.Value, Value, (w, v) => w.WriteAny(v));
+ if (Value is not null)
+ {
+ writer.WriteRequiredObject(OpenApiConstants.Value, Value, (w, v) => w.WriteAny(v));
+ }
// externalValue
writer.WriteProperty(OpenApiConstants.ExternalValue, ExternalValue);
diff --git a/src/Microsoft.OpenApi/Models/OpenApiMediaType.cs b/src/Microsoft.OpenApi/Models/OpenApiMediaType.cs
index 7520678e1..b901eacd1 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiMediaType.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiMediaType.cs
@@ -95,7 +95,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
// examples
if (Examples != null && Examples.Any())
{
- SerializeExamples(writer, Examples, callback);
+ writer.WriteOptionalMap(OpenApiConstants.Examples, Examples, callback);
}
// encoding
@@ -114,33 +114,5 @@ public virtual void SerializeAsV2(IOpenApiWriter writer)
{
// Media type does not exist in V2.
}
-
- private static void SerializeExamples(IOpenApiWriter writer, IDictionary examples, Action callback)
- {
- /* Special case for writing out empty arrays as valid response examples
- * Check if there is any example with an empty array as its value and set the flag `hasEmptyArray` to true
- * */
- var hasEmptyArray = examples.Values.Any( static example =>
- example.Value is JsonArray arr && arr.Count == 0
- );
-
- if (hasEmptyArray)
- {
- writer.WritePropertyName(OpenApiConstants.Examples);
- writer.WriteStartObject();
- foreach (var kvp in examples.Where(static kvp => kvp.Value.Value is JsonArray arr && arr.Count == 0))
- {
- writer.WritePropertyName(kvp.Key);
- writer.WriteStartObject();
- writer.WriteRequiredObject(OpenApiConstants.Value, kvp.Value.Value, (w, v) => w.WriteAny(v));
- writer.WriteEndObject();
- }
- writer.WriteEndObject();
- }
- else
- {
- writer.WriteOptionalMap(OpenApiConstants.Examples, examples, callback);
- }
- }
}
}
diff --git a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs
index 1ccfea863..7aa411b57 100644
--- a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs
+++ b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs
@@ -22,7 +22,7 @@ public abstract class OpenApiVisitorBase
public CurrentKeys CurrentKeys { get; } = new();
///
- /// Allow Rule to indicate validation error occured at a deeper context level.
+ /// Allow Rule to indicate validation error occurred at a deeper context level.
///
/// Identifier for context
public virtual void Enter(string segment)
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs
index 89f8f0a4b..8d32bea74 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
-using System;
-
namespace Microsoft.OpenApi
{
///
@@ -18,14 +16,13 @@ public static class OpenApiContactRules
new(nameof(EmailMustBeEmailFormat),
(context, item) =>
{
- context.Enter("email");
if (item is {Email: not null} && !item.Email.IsEmailAddress())
{
+ context.Enter("email");
context.CreateError(nameof(EmailMustBeEmailFormat),
- String.Format(SRResource.Validation_StringMustBeEmailAddress, item.Email));
+ string.Format(SRResource.Validation_StringMustBeEmailAddress, item.Email));
+ context.Exit();
}
- context.Exit();
});
-
}
}
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs
index d15b0a0a0..a1f166ee3 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs
@@ -19,13 +19,13 @@ public static class OpenApiDocumentRules
(context, item) =>
{
// info
- context.Enter("info");
if (item.Info == null)
{
+ context.Enter("info");
context.CreateError(nameof(OpenApiDocumentFieldIsMissing),
string.Format(SRResource.Validation_FieldIsRequired, "info", "document"));
+ context.Exit();
}
- context.Exit();
});
///
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiExtensionRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiExtensionRules.cs
index c9df09b2e..be927c364 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiExtensionRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiExtensionRules.cs
@@ -19,16 +19,16 @@ public static class OpenApiExtensibleRules
new(nameof(ExtensionNameMustStartWithXDash),
(context, item) =>
{
- context.Enter("extensions");
if (item.Extensions is not null)
{
+ context.Enter("extensions");
foreach (var extensible in item.Extensions.Keys.Where(static x => !x.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase)))
{
context.CreateError(nameof(ExtensionNameMustStartWithXDash),
string.Format(SRResource.Validation_ExtensionNameMustBeginWithXDash, extensible, context.PathString));
- }
+ }
+ context.Exit();
}
- context.Exit();
});
}
}
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiExternalDocsRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiExternalDocsRules.cs
index a15cd53b8..686b00c59 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiExternalDocsRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiExternalDocsRules.cs
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
-using System;
-
namespace Microsoft.OpenApi
{
///
@@ -19,15 +17,13 @@ public static class OpenApiExternalDocsRules
(context, item) =>
{
// url
- context.Enter("url");
if (item.Url == null)
{
+ context.Enter("url");
context.CreateError(nameof(UrlIsRequired),
- String.Format(SRResource.Validation_FieldIsRequired, "url", "External Documentation"));
+ string.Format(SRResource.Validation_FieldIsRequired, "url", "External Documentation"));
+ context.Exit();
}
- context.Exit();
});
-
- // add more rule.
}
}
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs
index ef7274c06..591c7695a 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
-using System;
-
namespace Microsoft.OpenApi
{
///
@@ -19,25 +17,22 @@ public static class OpenApiInfoRules
(context, item) =>
{
// title
- context.Enter("title");
if (item.Title == null)
{
+ context.Enter("title");
context.CreateError(nameof(InfoRequiredFields),
- String.Format(SRResource.Validation_FieldIsRequired, "title", "info"));
+ string.Format(SRResource.Validation_FieldIsRequired, "title", "info"));
+ context.Exit();
}
- context.Exit();
// version
- context.Enter("version");
if (item.Version == null)
{
+ context.Enter("version");
context.CreateError(nameof(InfoRequiredFields),
- String.Format(SRResource.Validation_FieldIsRequired, "version", "info"));
+ string.Format(SRResource.Validation_FieldIsRequired, "version", "info"));
+ context.Exit();
}
- context.Exit();
-
});
-
- // add more rule.
}
}
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiLicenseRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiLicenseRules.cs
index c9dc7e4a6..3d2c4d49e 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiLicenseRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiLicenseRules.cs
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
-using System;
-
namespace Microsoft.OpenApi
{
///
@@ -18,15 +16,13 @@ public static class OpenApiLicenseRules
new(nameof(LicenseRequiredFields),
(context, license) =>
{
- context.Enter("name");
if (license.Name == null)
{
+ context.Enter("name");
context.CreateError(nameof(LicenseRequiredFields),
- String.Format(SRResource.Validation_FieldIsRequired, "name", "license"));
+ string.Format(SRResource.Validation_FieldIsRequired, "name", "license"));
+ context.Exit();
}
- context.Exit();
});
-
- // add more rules
}
}
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiNonDefaultRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiNonDefaultRules.cs
index 5b3cd0a49..2bc019667 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiNonDefaultRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiNonDefaultRules.cs
@@ -36,7 +36,7 @@ public static class OpenApiNonDefaultRules
/// Validate the data matches with the given data type.
///
public static ValidationRule ParameterMismatchedDataType =>
- new(nameof(ParameterMismatchedDataType),
+ new(nameof(ParameterMismatchedDataType),
(context, parameter) =>
{
ValidateMismatchedDataType(context, nameof(ParameterMismatchedDataType), parameter.Example, parameter.Examples, parameter.Schema);
@@ -50,39 +50,33 @@ public static class OpenApiNonDefaultRules
(context, schema) =>
{
// default
- context.Enter("default");
-
if (schema.Default != null)
{
+ context.Enter("default");
RuleHelpers.ValidateDataTypeMismatch(context, nameof(SchemaMismatchedDataType), schema.Default, schema);
+ context.Exit();
}
- context.Exit();
-
// example
- context.Enter("example");
-
if (schema.Example != null)
{
+ context.Enter("example");
RuleHelpers.ValidateDataTypeMismatch(context, nameof(SchemaMismatchedDataType), schema.Example, schema);
+ context.Exit();
}
- context.Exit();
-
// enum
- context.Enter("enum");
-
if (schema.Enum != null)
{
+ context.Enter("enum");
for (var i = 0; i < schema.Enum.Count; i++)
{
context.Enter(i.ToString());
RuleHelpers.ValidateDataTypeMismatch(context, nameof(SchemaMismatchedDataType), schema.Enum[i], schema);
context.Exit();
}
+ context.Exit();
}
-
- context.Exit();
});
private static void ValidateMismatchedDataType(IValidationContext context,
@@ -92,20 +86,17 @@ private static void ValidateMismatchedDataType(IValidationContext context,
IOpenApiSchema? schema)
{
// example
- context.Enter("example");
-
if (example != null)
{
+ context.Enter("example");
RuleHelpers.ValidateDataTypeMismatch(context, ruleName, example, schema);
+ context.Exit();
}
- context.Exit();
-
// enum
- context.Enter("examples");
-
if (examples != null)
{
+ context.Enter("examples");
foreach (var key in examples.Keys.Where(k => examples[k] != null))
{
context.Enter(key);
@@ -114,9 +105,8 @@ private static void ValidateMismatchedDataType(IValidationContext context,
context.Exit();
context.Exit();
}
+ context.Exit();
}
-
- context.Exit();
}
}
}
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs
index 4f9efe831..10cd9f9a9 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
-using System;
-
namespace Microsoft.OpenApi
{
///
@@ -19,33 +17,31 @@ public static class OpenApiOAuthFlowRules
(context, flow) =>
{
// authorizationUrl
- context.Enter("authorizationUrl");
if (flow.AuthorizationUrl == null)
{
+ context.Enter("authorizationUrl");
context.CreateError(nameof(OAuthFlowRequiredFields),
- String.Format(SRResource.Validation_FieldIsRequired, "authorizationUrl", "OAuth Flow"));
+ string.Format(SRResource.Validation_FieldIsRequired, "authorizationUrl", "OAuth Flow"));
+ context.Exit();
}
- context.Exit();
// tokenUrl
- context.Enter("tokenUrl");
if (flow.TokenUrl == null)
{
+ context.Enter("tokenUrl");
context.CreateError(nameof(OAuthFlowRequiredFields),
- String.Format(SRResource.Validation_FieldIsRequired, "tokenUrl", "OAuth Flow"));
+ string.Format(SRResource.Validation_FieldIsRequired, "tokenUrl", "OAuth Flow"));
+ context.Exit();
}
- context.Exit();
// scopes
- context.Enter("scopes");
if (flow.Scopes == null)
{
+ context.Enter("scopes");
context.CreateError(nameof(OAuthFlowRequiredFields),
- String.Format(SRResource.Validation_FieldIsRequired, "scopes", "OAuth Flow"));
+ string.Format(SRResource.Validation_FieldIsRequired, "scopes", "OAuth Flow"));
+ context.Exit();
}
- context.Exit();
});
-
- // add more rule.
}
}
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs
index 2de42e02f..a84ea3255 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
-using System;
-
namespace Microsoft.OpenApi
{
///
@@ -19,22 +17,22 @@ public static class OpenApiParameterRules
(context, item) =>
{
// name
- context.Enter("name");
if (item.Name == null)
{
+ context.Enter("name");
context.CreateError(nameof(ParameterRequiredFields),
- String.Format(SRResource.Validation_FieldIsRequired, "name", "parameter"));
+ string.Format(SRResource.Validation_FieldIsRequired, "name", "parameter"));
+ context.Exit();
}
- context.Exit();
// in
- context.Enter("in");
if (item.In == null)
{
+ context.Enter("in");
context.CreateError(nameof(ParameterRequiredFields),
- String.Format(SRResource.Validation_FieldIsRequired, "in", "parameter"));
+ string.Format(SRResource.Validation_FieldIsRequired, "in", "parameter"));
+ context.Exit();
}
- context.Exit();
});
///
@@ -45,15 +43,14 @@ public static class OpenApiParameterRules
(context, item) =>
{
// required
- context.Enter("required");
if (item.In == ParameterLocation.Path && !item.Required)
{
+ context.Enter("required");
context.CreateError(
nameof(RequiredMustBeTrueWhenInIsPath),
"\"required\" must be true when parameter location is \"path\"");
+ context.Exit();
}
-
- context.Exit();
});
///
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs
index 9f1999e4c..378c46e36 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs
@@ -37,22 +37,22 @@ public static class OpenApiPathsRules
/// A relative path to an individual endpoint. The field name MUST begin with a slash.
///
public static ValidationRule PathMustBeUnique =>
- new ValidationRule(nameof(PathMustBeUnique),
+ new(nameof(PathMustBeUnique),
(context, item) =>
{
var hashSet = new HashSet();
foreach (var path in item.Keys)
{
- context.Enter(path);
-
var pathSignature = GetPathSignature(path);
-
+
if (!hashSet.Add(pathSignature))
+ {
+ context.Enter(path);
context.CreateError(nameof(PathMustBeUnique),
string.Format(SRResource.Validation_PathSignatureMustBeUnique, pathSignature));
-
- context.Exit();
+ context.Exit();
+ }
}
});
@@ -77,7 +77,5 @@ private static string GetPathSignature(string path)
return path;
}
-
- // add more rules
}
}
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiRecommendedRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiRecommendedRules.cs
new file mode 100644
index 000000000..b115b2ac1
--- /dev/null
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiRecommendedRules.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System.Net.Http;
+
+namespace Microsoft.OpenApi
+{
+ ///
+ /// Additional recommended validation rules for OpenAPI.
+ ///
+ public static class OpenApiRecommendedRules
+ {
+ ///
+ /// A relative path to an individual endpoint. The field name MUST begin with a slash.
+ ///
+ public static ValidationRule GetOperationShouldNotHaveRequestBody =>
+ new(nameof(GetOperationShouldNotHaveRequestBody),
+ (context, item) =>
+ {
+ foreach (var path in item)
+ {
+ if (path.Value.Operations is not { Count: > 0 } operations)
+ {
+ continue;
+ }
+
+ context.Enter(path.Key);
+
+ foreach (var operation in operations)
+ {
+ if (!operation.Key.Equals(HttpMethod.Get))
+ {
+ continue;
+ }
+
+ if (operation.Value.RequestBody != null)
+ {
+ context.Enter(operation.Key.Method.ToLowerInvariant());
+ context.Enter("requestBody");
+
+ context.CreateWarning(
+ nameof(GetOperationShouldNotHaveRequestBody),
+ "GET operations should not have a request body.");
+
+ context.Exit();
+ context.Exit();
+ }
+ }
+
+ context.Exit();
+ }
+ });
+ }
+}
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponseRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponseRules.cs
index c454b5290..1fe301ba3 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponseRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponseRules.cs
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
-using System;
-
namespace Microsoft.OpenApi
{
///
@@ -19,13 +17,13 @@ public static class OpenApiResponseRules
(context, response) =>
{
// description
- context.Enter("description");
if (response.Description == null)
{
+ context.Enter("description");
context.CreateError(nameof(ResponseRequiredFields),
- String.Format(SRResource.Validation_FieldIsRequired, "description", "response"));
+ string.Format(SRResource.Validation_FieldIsRequired, "description", "response"));
+ context.Exit();
}
- context.Exit();
});
// add more rule.
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponsesRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponsesRules.cs
index f1d2572ea..4c038b2c7 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponsesRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponsesRules.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using System;
-using System.Linq;
using System.Text.RegularExpressions;
namespace Microsoft.OpenApi
@@ -29,7 +28,7 @@ public static partial class OpenApiResponsesRules
new(nameof(ResponsesMustContainAtLeastOneResponse),
(context, responses) =>
{
- if (!responses.Keys.Any())
+ if (responses.Count == 0)
{
context.CreateError(nameof(ResponsesMustContainAtLeastOneResponse),
"Responses must contain at least one response");
@@ -45,8 +44,6 @@ public static partial class OpenApiResponsesRules
{
foreach (var key in responses.Keys)
{
- context.Enter(key);
-
if (!"default".Equals(key, StringComparison.OrdinalIgnoreCase) && !StatusCodeRegex
#if NET8_0_OR_GREATER
().IsMatch(key)
@@ -55,13 +52,13 @@ public static partial class OpenApiResponsesRules
#endif
)
{
+ context.Enter(key);
context.CreateError(nameof(ResponsesMustBeIdentifiedByDefaultOrStatusCode),
"Responses key must be 'default', an HTTP status code, " +
"or one of the following strings representing a range of HTTP status codes: " +
"'1XX', '2XX', '3XX', '4XX', '5XX' (case insensitive)");
+ context.Exit();
}
-
- context.Exit();
}
});
}
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs
index 81a4ab88f..70d558a13 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs
@@ -44,21 +44,19 @@ public static class OpenApiSchemaRules
(context, schema) =>
{
// discriminator
- context.Enter("discriminator");
-
if (schema is not null && schema.Discriminator != null)
{
var discriminatorName = schema.Discriminator?.PropertyName;
if (!ValidateChildSchemaAgainstDiscriminator(schema, discriminatorName))
{
+ context.Enter("discriminator");
context.CreateError(nameof(ValidateSchemaDiscriminator),
string.Format(SRResource.Validation_SchemaRequiredFieldListMustContainThePropertySpecifiedInTheDiscriminator,
schema is OpenApiSchemaReference { Reference: not null} schemaReference ? schemaReference.Reference.Id : string.Empty, discriminatorName));
+ context.Exit();
}
}
-
- context.Exit();
});
///
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs
index 95ea9490d..2843b9e33 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
-using System;
-
namespace Microsoft.OpenApi
{
///
@@ -18,41 +16,39 @@ public static class OpenApiServerRules
new(nameof(ServerRequiredFields),
(context, server) =>
{
- context.Enter("url");
if (server.Url == null)
{
+ context.Enter("url");
context.CreateError(nameof(ServerRequiredFields),
- String.Format(SRResource.Validation_FieldIsRequired, "url", "server"));
+ string.Format(SRResource.Validation_FieldIsRequired, "url", "server"));
+ context.Exit();
}
- context.Exit();
- context.Enter("variables");
if (server.Variables is not null)
{
+ context.Enter("variables");
foreach (var variable in server.Variables)
{
context.Enter(variable.Key);
ValidateServerVariableRequiredFields(context, variable.Key, variable.Value);
context.Exit();
- }
+ }
+ context.Exit();
}
- context.Exit();
});
- // add more rules
-
///
/// Validate required fields in server variable
///
private static void ValidateServerVariableRequiredFields(IValidationContext context, string key, OpenApiServerVariable item)
{
- context.Enter("default");
if (string.IsNullOrEmpty(item.Default))
{
+ context.Enter("default");
context.CreateError("ServerVariableMustHaveDefaultValue",
- String.Format(SRResource.Validation_FieldIsRequired, "default", key));
+ string.Format(SRResource.Validation_FieldIsRequired, "default", key));
+ context.Exit();
}
- context.Exit();
}
}
}
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiTagRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiTagRules.cs
index 45c9b7fda..dee4bc186 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiTagRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiTagRules.cs
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
-using System;
-
namespace Microsoft.OpenApi
{
///
@@ -18,15 +16,13 @@ public static class OpenApiTagRules
new(nameof(TagRequiredFields),
(context, tag) =>
{
- context.Enter("name");
if (tag.Name == null)
{
+ context.Enter("name");
context.CreateError(nameof(TagRequiredFields),
- String.Format(SRResource.Validation_FieldIsRequired, "name", "tag"));
+ string.Format(SRResource.Validation_FieldIsRequired, "name", "tag"));
+ context.Exit();
}
- context.Exit();
});
-
- // add more rules
}
}
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiMediaTypeTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiMediaTypeTests.cs
index a86a79412..dd669c889 100644
--- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiMediaTypeTests.cs
+++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiMediaTypeTests.cs
@@ -87,7 +87,16 @@ public async Task ParseMediaTypeWithEmptyArrayInExamplesWorks()
},
""examples"": {
""Success response - no results"": {
+ ""summary"": ""empty array summary"",
+ ""description"": ""empty array description"",
""value"": [ ]
+ },
+ ""Success response - with results"": {
+ ""summary"": ""array summary"",
+ ""description"": ""array description"",
+ ""value"": [
+ 1
+ ]
}
}
}
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiMediaType/examplesWithEmptyArray.json b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiMediaType/examplesWithEmptyArray.json
index 0d13dcaf2..c2b0a09e4 100644
--- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiMediaType/examplesWithEmptyArray.json
+++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiMediaType/examplesWithEmptyArray.json
@@ -12,7 +12,14 @@
},
"examples": {
"Success response - no results": {
+ "summary": "empty array summary",
+ "description": "empty array description",
"value": []
+ },
+ "Success response - with results": {
+ "summary": "array summary",
+ "description": "array description",
+ "value": [ 1 ]
}
}
}
\ No newline at end of file
diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
index 5733eb735..29e7ddc74 100644
--- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
+++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
@@ -1031,6 +1031,10 @@ namespace Microsoft.OpenApi
public OpenApiReaderException(string message, Microsoft.OpenApi.Reader.ParsingContext context) { }
public OpenApiReaderException(string message, System.Exception innerException) { }
}
+ public static class OpenApiRecommendedRules
+ {
+ public static Microsoft.OpenApi.ValidationRule GetOperationShouldNotHaveRequestBody { get; }
+ }
public class OpenApiReferenceError : Microsoft.OpenApi.OpenApiError
{
public readonly Microsoft.OpenApi.BaseOpenApiReference? Reference;
diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs
new file mode 100644
index 000000000..fc0b0df31
--- /dev/null
+++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs
@@ -0,0 +1,304 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using Xunit;
+
+namespace Microsoft.OpenApi.Validations.Tests;
+
+public static class OpenApiRecommendedRulesTests
+{
+ [Fact]
+ public static void GetOperationWithoutRequestBodyIsValid()
+ {
+ // Arrange
+ var document = new OpenApiDocument
+ {
+ Components = new OpenApiComponents(),
+ Info = new OpenApiInfo
+ {
+ Title = "People Document",
+ Version = "1.0.0"
+ },
+ Paths = [],
+ Workspace = new()
+ };
+
+ document.AddComponent("Person", new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ Properties = new Dictionary()
+ {
+ ["name"] = new OpenApiSchema { Type = JsonSchemaType.String },
+ ["email"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "email" }
+ }
+ });
+
+ document.Paths.Add("/people", new OpenApiPathItem
+ {
+ Operations = new Dictionary()
+ {
+ [HttpMethod.Get] = new OpenApiOperation
+ {
+ RequestBody = null,
+ Responses = new()
+ {
+ ["200"] = new OpenApiResponse
+ {
+ Description = "OK",
+ Content = new Dictionary()
+ {
+ ["application/json"] = new OpenApiMediaType
+ {
+ Schema = new OpenApiSchemaReference("Person", document),
+ }
+ }
+ }
+ }
+ },
+ [HttpMethod.Post] = new OpenApiOperation
+ {
+ RequestBody = new OpenApiRequestBody
+ {
+ Content = new Dictionary()
+ {
+ ["application/json"] = new OpenApiMediaType
+ {
+ Schema = new OpenApiSchemaReference("Person", document),
+ }
+ }
+ },
+ Responses = new()
+ {
+ ["200"] = new OpenApiResponse
+ {
+ Description = "OK",
+ Content = new Dictionary()
+ {
+ ["application/json"] = new OpenApiMediaType
+ {
+ Schema = new OpenApiSchemaReference("Person", document),
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+
+ var ruleSet = new ValidationRuleSet();
+ ruleSet.Add(typeof(OpenApiPaths), OpenApiRecommendedRules.GetOperationShouldNotHaveRequestBody);
+
+ // Act
+ var warnings = document.Validate(ruleSet);
+ var result = !warnings.Any();
+
+ // Assert
+ Assert.True(result);
+ Assert.NotNull(warnings);
+ Assert.Empty(warnings);
+ }
+
+ [Fact]
+ public static void GetOperationWithRequestBodyIsInvalid()
+ {
+ // Arrange
+ var document = new OpenApiDocument
+ {
+ Components = new OpenApiComponents(),
+ Info = new OpenApiInfo
+ {
+ Title = "People Document",
+ Version = "1.0.0"
+ },
+ Paths = [],
+ Workspace = new()
+ };
+
+ document.AddComponent("Person", new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ Properties = new Dictionary()
+ {
+ ["name"] = new OpenApiSchema { Type = JsonSchemaType.String },
+ ["email"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "email" }
+ }
+ });
+
+ document.Paths.Add("/people", new OpenApiPathItem
+ {
+ Operations = new Dictionary()
+ {
+ [HttpMethod.Get] = new OpenApiOperation
+ {
+ RequestBody = new OpenApiRequestBody
+ {
+ Content = new Dictionary()
+ {
+ ["application/json"] = new OpenApiMediaType
+ {
+ Schema = new OpenApiSchemaReference("Person", document),
+ }
+ }
+ },
+ Responses = new()
+ {
+ ["200"] = new OpenApiResponse
+ {
+ Description = "OK",
+ Content = new Dictionary()
+ {
+ ["application/json"] = new OpenApiMediaType
+ {
+ Schema = new OpenApiSchemaReference("Person", document),
+ }
+ }
+ }
+ }
+ },
+ [HttpMethod.Post] = new OpenApiOperation
+ {
+ RequestBody = new OpenApiRequestBody
+ {
+ Content = new Dictionary()
+ {
+ ["application/json"] = new OpenApiMediaType
+ {
+ Schema = new OpenApiSchemaReference("Person", document),
+ }
+ }
+ },
+ Responses = new()
+ {
+ ["200"] = new OpenApiResponse
+ {
+ Description = "OK",
+ Content = new Dictionary()
+ {
+ ["application/json"] = new OpenApiMediaType
+ {
+ Schema = new OpenApiSchemaReference("Person", document),
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+
+ var ruleSet = new ValidationRuleSet();
+ ruleSet.Add(typeof(OpenApiPaths), OpenApiRecommendedRules.GetOperationShouldNotHaveRequestBody);
+
+ // Act
+ var warnings = document.Validate(ruleSet);
+ var result = !warnings.Any();
+
+ // Assert
+ Assert.False(result);
+ Assert.NotNull(warnings);
+ var warning = Assert.Single(warnings);
+ Assert.Equal("GET operations should not have a request body.", warning.Message);
+ Assert.Equal("#/paths//people/get/requestBody", warning.Pointer);
+ }
+
+ [Fact]
+ public static void GetOperationWithRequestBodyIsValidUsingDefaultRuleSet()
+ {
+ // Arrange
+ var document = new OpenApiDocument
+ {
+ Components = new OpenApiComponents(),
+ Info = new OpenApiInfo
+ {
+ Title = "People Document",
+ Version = "1.0.0"
+ },
+ Paths = [],
+ Workspace = new()
+ };
+
+ document.AddComponent("Person", new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ Properties = new Dictionary()
+ {
+ ["name"] = new OpenApiSchema { Type = JsonSchemaType.String },
+ ["email"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "email" }
+ }
+ });
+
+ document.Paths.Add("/people", new OpenApiPathItem
+ {
+ Operations = new Dictionary()
+ {
+ [HttpMethod.Get] = new OpenApiOperation
+ {
+ RequestBody = new OpenApiRequestBody
+ {
+ Content = new Dictionary()
+ {
+ ["application/json"] = new OpenApiMediaType
+ {
+ Schema = new OpenApiSchemaReference("Person", document),
+ }
+ }
+ },
+ Responses = new()
+ {
+ ["200"] = new OpenApiResponse
+ {
+ Description = "OK",
+ Content = new Dictionary()
+ {
+ ["application/json"] = new OpenApiMediaType
+ {
+ Schema = new OpenApiSchemaReference("Person", document),
+ }
+ }
+ }
+ }
+ },
+ [HttpMethod.Post] = new OpenApiOperation
+ {
+ RequestBody = new OpenApiRequestBody
+ {
+ Content = new Dictionary()
+ {
+ ["application/json"] = new OpenApiMediaType
+ {
+ Schema = new OpenApiSchemaReference("Person", document),
+ }
+ }
+ },
+ Responses = new()
+ {
+ ["200"] = new OpenApiResponse
+ {
+ Description = "OK",
+ Content = new Dictionary()
+ {
+ ["application/json"] = new OpenApiMediaType
+ {
+ Schema = new OpenApiSchemaReference("Person", document),
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+
+ var ruleSet = ValidationRuleSet.GetDefaultRuleSet();
+
+ // Act
+ var warnings = document.Validate(ruleSet);
+ var result = !warnings.Any();
+
+ // Assert
+ Assert.True(result);
+ Assert.NotNull(warnings);
+ Assert.Empty(warnings);
+ }
+}