From dd5085a394d8ab23208c12be9bbcbd25bed6ce3f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 14:20:38 +0000
Subject: [PATCH 1/2] feat(library): preserve PatternProperties as
x-jsonschema-patternProperties extension for OpenAPI v2/v3.0 serialization
- Add PatternPropertiesExtension constant ("x-jsonschema-patternProperties")
- Emit extension when serializing to OpenAPI v2.0 or v3.0 and PatternProperties is non-empty
- Parse the extension back into PatternProperties when deserializing v2.0 or v3.0 documents
- Add unit tests for both serialization and deserialization round-trip
Co-authored-by: baywet <7905502+baywet@users.noreply.github.com>
---
.../Models/OpenApiConstants.cs | 5 +
src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 12 ++
src/Microsoft.OpenApi/PublicAPI.Unshipped.txt | 1 +
.../Reader/V2/OpenApiSchemaDeserializer.cs | 4 +
.../Reader/V3/OpenApiSchemaDeserializer.cs | 4 +
.../Models/OpenApiSchemaTests.cs | 136 ++++++++++++++++++
6 files changed, 162 insertions(+)
diff --git a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs
index 7af00afb7..5fdf13d12 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs
@@ -495,6 +495,11 @@ public static class OpenApiConstants
///
public const string PatternProperties = "patternProperties";
+ ///
+ /// Extension: x-jsonschema-patternProperties
+ ///
+ public const string PatternPropertiesExtension = "x-jsonschema-patternProperties";
+
///
/// Field: AdditionalProperties
///
diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
index 192aea29d..85f899bf5 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
@@ -556,6 +556,12 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
writer.WritePropertyName(OpenApiConstants.UnevaluatedPropertiesExtension);
writer.WriteValue(false);
}
+
+ // Write patternProperties as an extension
+ if (PatternProperties is { Count: > 0 })
+ {
+ writer.WriteOptionalMap(OpenApiConstants.PatternPropertiesExtension, PatternProperties, callback);
+ }
}
// extensions
@@ -836,6 +842,12 @@ private void SerializeAsV2(
writer.WriteValue(false);
}
+ // Write patternProperties as an extension
+ if (PatternProperties is { Count: > 0 })
+ {
+ writer.WriteOptionalMap(OpenApiConstants.PatternPropertiesExtension, PatternProperties, (w, s) => s.SerializeAsV2(w));
+ }
+
// extensions
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0);
diff --git a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt
index 7dc5c5811..8513259ea 100644
--- a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt
@@ -1 +1,2 @@
#nullable enable
+const Microsoft.OpenApi.OpenApiConstants.PatternPropertiesExtension = "x-jsonschema-patternProperties" -> string!
diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs
index 028014664..8c4104cc3 100644
--- a/src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs
+++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs
@@ -241,6 +241,10 @@ internal static partial class OpenApiV2Deserializer
"example",
(o, n, _) => o.Example = n.CreateAny()
},
+ {
+ OpenApiConstants.PatternPropertiesExtension,
+ (o, n, t) => o.PatternProperties = n.CreateMap(LoadSchema, t)
+ },
};
private static readonly PatternFieldMap _openApiSchemaPatternFields = new PatternFieldMap
diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs
index fd73cec99..804c8567e 100644
--- a/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs
+++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs
@@ -276,6 +276,10 @@ internal static partial class OpenApiV3Deserializer
}
}
},
+ {
+ OpenApiConstants.PatternPropertiesExtension,
+ (o, n, t) => o.PatternProperties = n.CreateMap(LoadSchema, t)
+ },
};
private static readonly PatternFieldMap _openApiSchemaPatternFields = new()
diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
index d5e785eac..93ce82c2d 100644
--- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
+++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
@@ -1308,6 +1308,142 @@ public async Task SerializeUnevaluatedPropertiesTrueNotEmittedInEarlierVersions(
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}
+ // PatternProperties tests
+ [Theory]
+ [InlineData(OpenApiSpecVersion.OpenApi3_1)]
+ [InlineData(OpenApiSpecVersion.OpenApi3_2)]
+ public async Task SerializePatternPropertiesAsKeywordInV31AndV32(OpenApiSpecVersion version)
+ {
+ var expected = @"{ ""patternProperties"": { ""^[a-z]+"": { ""type"": ""string"" } } }";
+ // Given - patternProperties should be emitted as a standard keyword in v3.1+
+ var schema = new OpenApiSchema
+ {
+ PatternProperties = new Dictionary
+ {
+ ["^[a-z]+"] = new OpenApiSchema { Type = JsonSchemaType.String }
+ }
+ };
+
+ // When
+ var actual = await schema.SerializeAsJsonAsync(version);
+
+ // Then
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
+ }
+
+ [Theory]
+ [InlineData(OpenApiSpecVersion.OpenApi2_0)]
+ [InlineData(OpenApiSpecVersion.OpenApi3_0)]
+ public async Task SerializePatternPropertiesAsExtensionInEarlierVersions(OpenApiSpecVersion version)
+ {
+ var expected = @"{ ""x-jsonschema-patternProperties"": { ""^[a-z]+"": { ""type"": ""string"" } } }";
+ // Given - patternProperties should be emitted as extension in versions < 3.1
+ var schema = new OpenApiSchema
+ {
+ PatternProperties = new Dictionary
+ {
+ ["^[a-z]+"] = new OpenApiSchema { Type = JsonSchemaType.String }
+ }
+ };
+
+ // When
+ var actual = await schema.SerializeAsJsonAsync(version);
+
+ // Then
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
+ }
+
+ [Theory]
+ [InlineData(OpenApiSpecVersion.OpenApi2_0)]
+ [InlineData(OpenApiSpecVersion.OpenApi3_0)]
+ public async Task SerializeEmptyPatternPropertiesNotEmittedInEarlierVersions(OpenApiSpecVersion version)
+ {
+ var expected = @"{ }";
+ // Given - empty patternProperties should not emit extension
+ var schema = new OpenApiSchema
+ {
+ PatternProperties = new Dictionary()
+ };
+
+ // When
+ var actual = await schema.SerializeAsJsonAsync(version);
+
+ // Then
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
+ }
+
+ [Fact]
+ public void DeserializePatternPropertiesExtensionInV2AssignsPatternPropertiesProperty()
+ {
+ // Given - a V2 document with x-jsonschema-patternProperties extension in a definition
+ var jsonContent = """
+ {
+ "swagger": "2.0",
+ "info": { "title": "Test", "version": "1.0" },
+ "paths": {},
+ "definitions": {
+ "TestSchema": {
+ "type": "object",
+ "x-jsonschema-patternProperties": {
+ "^[a-z]+": { "type": "string" }
+ }
+ }
+ }
+ }
+ """;
+
+ // When
+ var readResult = OpenApiDocument.Parse(jsonContent, "json");
+
+ // Then
+ Assert.Empty(readResult.Diagnostic.Errors);
+ var schema = readResult.Document.Components.Schemas["TestSchema"];
+ Assert.NotNull(schema);
+ Assert.NotNull(schema.PatternProperties);
+ Assert.Single(schema.PatternProperties);
+ Assert.True(schema.PatternProperties.ContainsKey("^[a-z]+"));
+ Assert.Equal(JsonSchemaType.String, schema.PatternProperties["^[a-z]+"].Type);
+ // Extension should NOT be present on the schema (it was consumed)
+ Assert.True(schema.Extensions is null || !schema.Extensions.ContainsKey("x-jsonschema-patternProperties"));
+ }
+
+ [Fact]
+ public void DeserializePatternPropertiesExtensionInV3AssignsPatternPropertiesProperty()
+ {
+ // Given - a V3 document with x-jsonschema-patternProperties extension in a component schema
+ var jsonContent = """
+ {
+ "openapi": "3.0.0",
+ "info": { "title": "Test", "version": "1.0" },
+ "paths": {},
+ "components": {
+ "schemas": {
+ "TestSchema": {
+ "type": "object",
+ "x-jsonschema-patternProperties": {
+ "^[a-z]+": { "type": "string" }
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ // When
+ var readResult = OpenApiDocument.Parse(jsonContent, "json");
+
+ // Then
+ Assert.Empty(readResult.Diagnostic.Errors);
+ var schema = readResult.Document.Components.Schemas["TestSchema"];
+ Assert.NotNull(schema);
+ Assert.NotNull(schema.PatternProperties);
+ Assert.Single(schema.PatternProperties);
+ Assert.True(schema.PatternProperties.ContainsKey("^[a-z]+"));
+ Assert.Equal(JsonSchemaType.String, schema.PatternProperties["^[a-z]+"].Type);
+ // Extension should NOT be present on the schema (it was consumed)
+ Assert.True(schema.Extensions is null || !schema.Extensions.ContainsKey("x-jsonschema-patternProperties"));
+ }
+
internal class SchemaVisitor : OpenApiVisitorBase
{
public List Titles = new();
From 26f72707f02a3e40e869f6c0a780ef6f1c0f9b6f Mon Sep 17 00:00:00 2001
From: Vincent Biret
Date: Thu, 5 Mar 2026 10:24:23 -0500
Subject: [PATCH 2/2] chore: removes 32 unit test
---
test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
index 93ce82c2d..1e924cad3 100644
--- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
+++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
@@ -1311,7 +1311,6 @@ public async Task SerializeUnevaluatedPropertiesTrueNotEmittedInEarlierVersions(
// PatternProperties tests
[Theory]
[InlineData(OpenApiSpecVersion.OpenApi3_1)]
- [InlineData(OpenApiSpecVersion.OpenApi3_2)]
public async Task SerializePatternPropertiesAsKeywordInV31AndV32(OpenApiSpecVersion version)
{
var expected = @"{ ""patternProperties"": { ""^[a-z]+"": { ""type"": ""string"" } } }";