Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,11 @@ public static class OpenApiConstants
/// </summary>
public const string PatternProperties = "patternProperties";

/// <summary>
/// Extension: x-jsonschema-patternProperties
/// </summary>
public const string PatternPropertiesExtension = "x-jsonschema-patternProperties";

/// <summary>
/// Field: AdditionalProperties
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@
}
}

private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,

Check failure on line 389 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 19 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET&issues=AZy-oTFBZK16s942-euA&open=AZy-oTFBZK16s942-euA&pullRequest=2758
Action<IOpenApiWriter, IOpenApiSerializable> callback)
{
writer.WriteStartObject();
Expand Down Expand Up @@ -556,6 +556,12 @@
writer.WritePropertyName(OpenApiConstants.UnevaluatedPropertiesExtension);
writer.WriteValue(false);
}

// Write patternProperties as an extension
if (PatternProperties is { Count: > 0 })
{
writer.WriteOptionalMap(OpenApiConstants.PatternPropertiesExtension, PatternProperties, callback);
}
}

// extensions
Expand Down Expand Up @@ -681,7 +687,7 @@
/// <param name="writer">The open api writer.</param>
/// <param name="parentRequiredProperties">The list of required properties in parent schema.</param>
/// <param name="propertyName">The property name that will be serialized.</param>
private void SerializeAsV2(

Check failure on line 690 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 19 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET&issues=AZy-oTFBZK16s942-euB&open=AZy-oTFBZK16s942-euB&pullRequest=2758
IOpenApiWriter writer,
ISet<string>? parentRequiredProperties,
string? propertyName)
Expand Down Expand Up @@ -836,6 +842,12 @@
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);

Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.OpenApi/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
const Microsoft.OpenApi.OpenApiConstants.PatternPropertiesExtension = "x-jsonschema-patternProperties" -> string!
Original file line number Diff line number Diff line change
Expand Up @@ -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<OpenApiSchema> _openApiSchemaPatternFields = new PatternFieldMap<OpenApiSchema>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,10 @@ internal static partial class OpenApiV3Deserializer
}
}
},
{
OpenApiConstants.PatternPropertiesExtension,
(o, n, t) => o.PatternProperties = n.CreateMap(LoadSchema, t)
},
};

private static readonly PatternFieldMap<OpenApiSchema> _openApiSchemaPatternFields = new()
Expand Down
135 changes: 135 additions & 0 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,141 @@ public async Task SerializeUnevaluatedPropertiesTrueNotEmittedInEarlierVersions(
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

// PatternProperties tests
[Theory]
[InlineData(OpenApiSpecVersion.OpenApi3_1)]
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<string, IOpenApiSchema>
{
["^[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<string, IOpenApiSchema>
{
["^[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<string, IOpenApiSchema>()
};

// 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<string> Titles = new();
Expand Down
Loading