diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 534c8ff8b..0befe35d6 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "3.1.3"
+ ".": "3.3.1"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 44e6eba95..3d8605bbf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,40 @@
# Changelog
+## [3.3.1](https://github.com/microsoft/OpenAPI.NET/compare/v3.3.0...v3.3.1) (2026-01-22)
+
+
+### Features
+
+* **models:** add shared Content interface ([#2695](https://github.com/microsoft/OpenAPI.NET/issues/2695)) ([338566f](https://github.com/microsoft/OpenAPI.NET/commit/338566fafce04ee1329f4ead61fe1e87e01144ad))
+
+
+### Bug Fixes
+
+* broken binary compatibility due to interface changes in previous version ([d96bba7](https://github.com/microsoft/OpenAPI.NET/commit/d96bba72c383cd5db2b7032530aee3b4d944ebc6))
+
+## [3.3.0](https://github.com/microsoft/OpenAPI.NET/compare/v3.2.0...v3.3.0) (2026-01-21)
+
+
+### Features
+
+* **models:** add shared Content interface ([9e13b25](https://github.com/microsoft/OpenAPI.NET/commit/9e13b2574e68387ecc01c453b571208c40124b46))
+* **models:** add shared Content interface ([#2695](https://github.com/microsoft/OpenAPI.NET/issues/2695)) ([9e13b25](https://github.com/microsoft/OpenAPI.NET/commit/9e13b2574e68387ecc01c453b571208c40124b46))
+* **models:** support mutualTLS security scheme ([a4efdfe](https://github.com/microsoft/OpenAPI.NET/commit/a4efdfe43ee310fba4bb643d9fd28ce92da32338))
+
+## [3.2.0](https://github.com/microsoft/OpenAPI.NET/compare/v3.1.3...v3.2.0) (2026-01-19)
+
+
+### Features
+
+* hidi validate command now logs warnings ([76a3c0f](https://github.com/microsoft/OpenAPI.NET/commit/76a3c0fe33a6c953263d9d91669b2f1bab562a79))
+* hidi validate command now logs warnings ([62e7d56](https://github.com/microsoft/OpenAPI.NET/commit/62e7d56ac0863875999240d68a2766d2cc2d594c))
+
+
+### Bug Fixes
+
+* discriminator property validation fails any/allOf cases when it shouldn't ([fb6cecc](https://github.com/microsoft/OpenAPI.NET/commit/fb6cecccafd5713bc1eb22e0cf07619cf495ebb5))
+* discriminator property validation fails any/allOf cases when it shouldn't ([a8fb81c](https://github.com/microsoft/OpenAPI.NET/commit/a8fb81cf9524a3f2f721aa808db244434e1cd177))
+
## [3.1.3](https://github.com/microsoft/OpenAPI.NET/compare/v3.1.2...v3.1.3) (2026-01-16)
diff --git a/Directory.Build.props b/Directory.Build.props
index b0681cf72..13c72dd49 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
- 3.1.3
+ 3.3.1
diff --git a/docs/upgrade-guide-2.md b/docs/upgrade-guide-2.md
index 14eaacdf0..752b60c95 100644
--- a/docs/upgrade-guide-2.md
+++ b/docs/upgrade-guide-2.md
@@ -32,7 +32,7 @@ One of the key features of OpenAPI.NET is its performance. This version makes it
In v1, instances of `$ref` were resolved in a second pass of the document to ensure the target of the reference has been parsed before attempting to resolve it. In v2, reference targets are lazily resolved when reference objects are accessed. This improves load time performance for documents that make heavy use of references.
-[How does this change the behavior of external references?]
+Because references are lazily loaded and depend on the workspace context, loading a document with unresolved references (internal or external) does not lead to an exception being thrown anymore. Instead warnings are logged in the diagnostics object. This gives you an opportunity to load additional documents in the workspace if needed, [more information](#component-registration-in-a-documents-workspace).
### Results
diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs
index 48f1d1c3e..a494b4f2e 100644
--- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs
+++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs
@@ -398,6 +398,7 @@ private static async Task ParseOpenApiAsync(string openApiFile, bool
logger.LogTrace("{Timestamp}ms: Completed parsing.", stopwatch.ElapsedMilliseconds);
LogErrors(logger, result);
+ LogWarnings(logger, result);
stopwatch.Stop();
}
@@ -652,7 +653,7 @@ private static string GetInputPathExtension(string? openapi = null, string? csdl
private static void LogErrors(ILogger logger, ReadResult result)
{
var context = result.Diagnostic;
- if (context is not null && context.Errors.Count != 0)
+ if (context is { Errors.Count: > 0 })
{
using (logger.BeginScope("Detected errors"))
{
@@ -664,6 +665,21 @@ private static void LogErrors(ILogger logger, ReadResult result)
}
}
+ private static void LogWarnings(ILogger logger, ReadResult result)
+ {
+ var context = result.Diagnostic;
+ if (context is { Warnings.Count: > 0 })
+ {
+ using (logger.BeginScope("Detected warnings"))
+ {
+ foreach (var warning in context.Warnings)
+ {
+ logger.LogWarning("Detected warning during parsing: {Warning}", warning.ToString());
+ }
+ }
+ }
+ }
+
internal static void WriteTreeDocumentAsMarkdown(string openapiUrl, OpenApiDocument document, StreamWriter writer)
{
var rootNode = OpenApiUrlTreeNode.Create(document, "main");
diff --git a/src/Microsoft.OpenApi/Interfaces/IOpenApiReadOnlyExtensible.cs b/src/Microsoft.OpenApi/Interfaces/IOpenApiReadOnlyExtensible.cs
index fac742d7d..c059842c0 100644
--- a/src/Microsoft.OpenApi/Interfaces/IOpenApiReadOnlyExtensible.cs
+++ b/src/Microsoft.OpenApi/Interfaces/IOpenApiReadOnlyExtensible.cs
@@ -3,7 +3,7 @@
namespace Microsoft.OpenApi;
///
-/// Represents an Extensible Open API element elements can be rad from.
+/// Represents an Extensible Open API element elements can be read from.
///
public interface IOpenApiReadOnlyExtensible
{
diff --git a/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs b/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs
index 4bee59619..57603e0aa 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs
@@ -126,6 +126,14 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
// openIdConnectUrl
writer.WriteProperty(OpenApiConstants.OpenIdConnectUrl, OpenIdConnectUrl?.ToString());
break;
+ case SecuritySchemeType.MutualTLS:
+ // No additional properties for mutualTLS
+ if (version < OpenApiSpecVersion.OpenApi3_1)
+ {
+ // mutualTLS is introduced in OpenAPI 3.1
+ throw new OpenApiException($"mutualTLS security scheme is only supported in OpenAPI 3.1 and later versions. Current version: {version}");
+ }
+ break;
}
// deprecated - serialize as native field for v3.2+ or as extension for earlier versions
@@ -170,6 +178,14 @@ public virtual void SerializeAsV2(IOpenApiWriter writer)
return;
}
+ if (Type == SecuritySchemeType.MutualTLS)
+ {
+ // Bail because V2 does not support mutualTLS
+ writer.WriteStartObject();
+ writer.WriteEndObject();
+ return;
+ }
+
writer.WriteStartObject();
// type
diff --git a/src/Microsoft.OpenApi/Models/SecuritySchemeType.cs b/src/Microsoft.OpenApi/Models/SecuritySchemeType.cs
index 6c304597a..5640caa9a 100644
--- a/src/Microsoft.OpenApi/Models/SecuritySchemeType.cs
+++ b/src/Microsoft.OpenApi/Models/SecuritySchemeType.cs
@@ -26,6 +26,11 @@ public enum SecuritySchemeType
///
/// Use OAuth2 with OpenId Connect URL to discover OAuth2 configuration value.
///
- [Display("openIdConnect")] OpenIdConnect
+ [Display("openIdConnect")] OpenIdConnect,
+
+ ///
+ /// Use mutual TLS authentication.
+ ///
+ [Display("mutualTLS")] MutualTLS
}
}
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs
index 70d558a13..4f2a122a9 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs
@@ -5,6 +5,8 @@
namespace Microsoft.OpenApi
{
+ using System;
+ using System.ComponentModel;
using System.Linq;
///
@@ -48,6 +50,7 @@ public static class OpenApiSchemaRules
{
var discriminatorName = schema.Discriminator?.PropertyName;
+#pragma warning disable CS0618 // Type or member is obsolete
if (!ValidateChildSchemaAgainstDiscriminator(schema, discriminatorName))
{
context.Enter("discriminator");
@@ -56,6 +59,7 @@ public static class OpenApiSchemaRules
schema is OpenApiSchemaReference { Reference: not null} schemaReference ? schemaReference.Reference.Id : string.Empty, discriminatorName));
context.Exit();
}
+#pragma warning restore CS0618 // Type or member is obsolete
}
});
@@ -65,6 +69,8 @@ public static class OpenApiSchemaRules
/// The parent schema.
/// Adds support for polymorphism. The discriminator is an object name that is used to differentiate
/// between other schemas which may satisfy the payload description.
+ [Obsolete("This method will be made private in future versions.")]
+ [Browsable(false)]
public static bool ValidateChildSchemaAgainstDiscriminator(IOpenApiSchema schema, string? discriminatorName)
{
if (discriminatorName is not null)
@@ -72,15 +78,15 @@ public static bool ValidateChildSchemaAgainstDiscriminator(IOpenApiSchema schema
if (schema.Required is null || !schema.Required.Contains(discriminatorName))
{
// recursively check nested schema.OneOf, schema.AnyOf or schema.AllOf and their required fields for the discriminator
- if (schema.OneOf?.Count != 0)
+ if (schema.OneOf is { Count: > 0})
{
return TraverseSchemaElements(discriminatorName, schema.OneOf);
}
- if (schema.AnyOf?.Count != 0)
+ if (schema.AnyOf is { Count: > 0})
{
return TraverseSchemaElements(discriminatorName, schema.AnyOf);
}
- if (schema.AllOf?.Count != 0)
+ if (schema.AllOf is { Count: > 0})
{
return TraverseSchemaElements(discriminatorName, schema.AllOf);
}
@@ -102,25 +108,26 @@ public static bool ValidateChildSchemaAgainstDiscriminator(IOpenApiSchema schema
/// between other schemas which may satisfy the payload description.
/// The child schema.
///
+ [Obsolete("This method will be made private in future versions.")]
+ [Browsable(false)]
public static bool TraverseSchemaElements(string discriminatorName, IList? childSchema)
{
- if (childSchema is not null)
+ if (childSchema is null)
{
- foreach (var childItem in childSchema)
+ return false;
+ }
+ foreach (var childItem in childSchema)
+ {
+ if ((!childItem.Properties?.ContainsKey(discriminatorName) ?? false) &&
+ (!childItem.Required?.Contains(discriminatorName) ?? false))
{
- if ((!childItem.Properties?.ContainsKey(discriminatorName) ?? false) &&
- (!childItem.Required?.Contains(discriminatorName) ?? false))
- {
- return ValidateChildSchemaAgainstDiscriminator(childItem, discriminatorName);
- }
- else
- {
- return true;
- }
+ return ValidateChildSchemaAgainstDiscriminator(childItem, discriminatorName);
}
- return false;
- }
-
+ else
+ {
+ return true;
+ }
+ }
return false;
}
}
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSecuritySchemeTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSecuritySchemeTests.cs
index e053d7405..ea0937237 100644
--- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSecuritySchemeTests.cs
+++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSecuritySchemeTests.cs
@@ -102,6 +102,25 @@ public async Task ParseOpenIdConnectSecuritySchemeShouldSucceed()
}, securityScheme);
}
+ [Fact]
+ public async Task ParseMutualTlsSecuritySchemeShouldSucceed()
+ {
+ // Act
+ var securityScheme = await OpenApiModelFactory.LoadAsync(
+ Path.Combine(SampleFolderPath, "mutualTlsSecurityScheme.yaml"),
+ OpenApiSpecVersion.OpenApi3_2,
+ new(),
+ SettingsFixture.ReaderSettings);
+
+ // Assert
+ Assert.Equivalent(
+ new OpenApiSecurityScheme
+ {
+ Type = SecuritySchemeType.MutualTLS,
+ Description = "Sample Description"
+ }, securityScheme);
+ }
+
[Fact]
public async Task ParseOAuth2SecuritySchemeWithDeviceAuthorizationUrlShouldSucceed()
{
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSecurityScheme/mutualTlsSecurityScheme.yaml b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSecurityScheme/mutualTlsSecurityScheme.yaml
new file mode 100644
index 000000000..72b4e9ae8
--- /dev/null
+++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSecurityScheme/mutualTlsSecurityScheme.yaml
@@ -0,0 +1,2 @@
+type: mutualTLS
+description: Sample Description
diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs
index 1432d07cc..cd8499b9f 100644
--- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs
+++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs
@@ -101,6 +101,12 @@ public class OpenApiSecuritySchemeTests
OpenIdConnectUrl = new("https://example.com/openIdConnect")
};
+ private static OpenApiSecurityScheme MutualTlsSecurityScheme => new()
+ {
+ Description = "description1",
+ Type = SecuritySchemeType.MutualTLS
+ };
+
private static OpenApiSecuritySchemeReference OpenApiSecuritySchemeReference => new("sampleSecurityScheme");
private static OpenApiSecurityScheme ReferencedSecurityScheme => new()
{
@@ -208,6 +214,19 @@ public async Task SerializeHttpBearerSecuritySchemeAsV3JsonWorks()
Assert.Equal(expected, actual);
}
+ [Fact]
+ public void SerializeMutualTlsSecuritySchemeAsV3Throws()
+ {
+ // Arrange
+ var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
+ var writer = new OpenApiJsonWriter(outputStringWriter);
+
+ // Act & Assert
+ var exception = Assert.Throws(() => MutualTlsSecurityScheme.SerializeAsV3(writer));
+ Assert.Contains("mutualTLS security scheme is only supported in OpenAPI 3.1 and later versions", exception.Message);
+ Assert.Contains($"Current version: {OpenApiSpecVersion.OpenApi3_0}", exception.Message);
+ }
+
[Fact]
public async Task SerializeOAuthSingleFlowSecuritySchemeAsV3JsonWorks()
{
diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
index 1446cb8d7..59f1bce31 100644
--- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
+++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
@@ -1352,7 +1352,11 @@ namespace Microsoft.OpenApi
{
public static Microsoft.OpenApi.ValidationRule ValidateSchemaDiscriminator { get; }
public static Microsoft.OpenApi.ValidationRule ValidateSchemaPropertyHasValue { get; }
+ [System.ComponentModel.Browsable(false)]
+ [System.Obsolete("This method will be made private in future versions.")]
public static bool TraverseSchemaElements(string discriminatorName, System.Collections.Generic.IList? childSchema) { }
+ [System.ComponentModel.Browsable(false)]
+ [System.Obsolete("This method will be made private in future versions.")]
public static bool ValidateChildSchemaAgainstDiscriminator(Microsoft.OpenApi.IOpenApiSchema schema, string? discriminatorName) { }
}
public class OpenApiSecurityRequirement : System.Collections.Generic.Dictionary>, Microsoft.OpenApi.IOpenApiElement, Microsoft.OpenApi.IOpenApiSerializable
@@ -1929,6 +1933,8 @@ namespace Microsoft.OpenApi
OAuth2 = 2,
[Microsoft.OpenApi.Display("openIdConnect")]
OpenIdConnect = 3,
+ [Microsoft.OpenApi.Display("mutualTLS")]
+ MutualTLS = 4,
}
public abstract class SourceExpression : Microsoft.OpenApi.RuntimeExpression
{
diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs
index f22806825..fdf36e0b4 100644
--- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs
+++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs
@@ -246,7 +246,7 @@ public void ValidateSchemaRequiredFieldListMustContainThePropertySpecifiedInTheD
}
[Fact]
- public void ValidateOneOfSchemaPropertyNameContainsPropertySpecifiedInTheDiscriminator()
+ public void ValidateOneOfSchemaPropertyNameContainsPropertySpecifiedInTheDiscriminatorOneOf()
{
// Arrange
var components = new OpenApiComponents
@@ -293,5 +293,102 @@ public void ValidateOneOfSchemaPropertyNameContainsPropertySpecifiedInTheDiscrim
//Assert
Assert.Empty(errors);
}
+
+ [Fact]
+ public void ValidateOneOfSchemaPropertyNameContainsPropertySpecifiedInTheDiscriminatorAnyOf()
+ {
+ // Arrange
+ var components = new OpenApiComponents
+ {
+ Schemas = new Dictionary
+ {
+ {
+ "Person",
+ new OpenApiSchema
+ {
+ Type = JsonSchemaType.Array,
+ Discriminator = new()
+ {
+ PropertyName = "type"
+ },
+ AnyOf =
+ [
+ new OpenApiSchema()
+ {
+ Properties = new Dictionary
+ {
+ {
+ "type",
+ new OpenApiSchema
+ {
+ Type = JsonSchemaType.Array
+ }
+ }
+ },
+ }
+ ],
+ }
+ }
+ }
+ };
+
+ // Act
+ var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet());
+ var walker = new OpenApiWalker(validator);
+ walker.Walk(components);
+
+ var errors = validator.Errors;
+
+ //Assert
+ Assert.Empty(errors);
+ }
+ [Fact]
+ public void ValidateOneOfSchemaPropertyNameContainsPropertySpecifiedInTheDiscriminatorAllOf()
+ {
+ // Arrange
+ var components = new OpenApiComponents
+ {
+ Schemas = new Dictionary
+ {
+ {
+ "Person",
+ new OpenApiSchema
+ {
+ Type = JsonSchemaType.Array,
+ Discriminator = new()
+ {
+ PropertyName = "type"
+ },
+ AllOf =
+ [
+ new OpenApiSchema()
+ {
+ Properties = new Dictionary
+ {
+ {
+ "type",
+ new OpenApiSchema
+ {
+ Type = JsonSchemaType.Array
+ }
+ }
+ },
+ }
+ ],
+ }
+ }
+ }
+ };
+
+ // Act
+ var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet());
+ var walker = new OpenApiWalker(validator);
+ walker.Walk(components);
+
+ var errors = validator.Errors;
+
+ //Assert
+ Assert.Empty(errors);
+ }
}
}