From 1a974f8dfcd7850c70d80133ceecee08f6671cd7 Mon Sep 17 00:00:00 2001 From: Romain Vergnory Date: Tue, 9 Jun 2026 18:26:47 +0200 Subject: [PATCH 1/5] feat: add contains/minContains/maxContains members --- .../IOpenApiSchemaWithContainsProperties.cs | 31 ++++++ .../Models/OpenApiConstants.cs | 15 +++ src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 26 ++++- .../References/OpenApiSchemaReference.cs | 8 +- src/Microsoft.OpenApi/PublicAPI.Unshipped.txt | 16 +++ .../Reader/V31/OpenApiSchemaDeserializer.cs | 26 +++++ .../Reader/V32/OpenApiSchemaDeserializer.cs | 26 +++++ .../V31Tests/OpenApiSchemaTests.cs | 8 +- .../Samples/OpenApiSchema/jsonSchema.json | 7 +- .../V32Tests/OpenApiSchemaTests.cs | 8 +- .../Samples/OpenApiSchema/jsonSchema.json | 7 +- .../Models/OpenApiSchemaTests.cs | 104 ++++++++++++++++++ 12 files changed, 276 insertions(+), 6 deletions(-) create mode 100644 src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchemaWithContainsProperties.cs diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchemaWithContainsProperties.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchemaWithContainsProperties.cs new file mode 100644 index 000000000..2aa91d0d6 --- /dev/null +++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchemaWithContainsProperties.cs @@ -0,0 +1,31 @@ +namespace Microsoft.OpenApi; + +/// +/// Compatibility interface for the JSON Schema 2020-12 "contains" keywords support. +/// This interface provides access to the Contains, MaxContains and MinContains properties, which were +/// missed in the initial release of the IOpenApiSchema interface. +/// +/// This is a temporary compatibility solution. In the next major version this interface should be +/// merged into IOpenApiSchema. +/// +public interface IOpenApiSchemaWithContainsProperties +{ + /// + /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-core#name-contains + /// An array instance is valid against "contains" if at least one of its elements is valid against this schema. + /// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema. + /// + IOpenApiSchema? Contains { get; } + + /// + /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation + /// The number of elements matching the "contains" schema MUST be less than or equal to this value. + /// + uint? MaxContains { get; } + + /// + /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation + /// The number of elements matching the "contains" schema MUST be greater than or equal to this value. + /// + uint? MinContains { get; } +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs index a54758002..cac501a89 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs @@ -485,6 +485,21 @@ public static class OpenApiConstants /// public const string UniqueItems = "uniqueItems"; + /// + /// Field: Contains + /// + public const string Contains = "contains"; + + /// + /// Field: MaxContains + /// + public const string MaxContains = "maxContains"; + + /// + /// Field: MinContains + /// + public const string MinContains = "minContains"; + /// /// Field: MaxProperties /// diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 40f24bdd0..f90449d28 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -18,7 +18,7 @@ namespace Microsoft.OpenApi /// - Serialization: To produce something functionally equivalent to boolean schemas, create an empty /// for "true" behavior, or create a schema with only set to an empty schema for "false" behavior. /// - public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties, IMetadataContainer + public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties, IOpenApiSchemaWithContainsProperties, IMetadataContainer { /// public string? Title { get; set; } @@ -207,6 +207,15 @@ public string? Minimum /// public bool? UniqueItems { get; set; } + /// + public IOpenApiSchema? Contains { get; set; } + + /// + public uint? MaxContains { get; set; } + + /// + public uint? MinContains { get; set; } + /// public IDictionary? Properties { get; set; } @@ -318,6 +327,12 @@ internal OpenApiSchema(IOpenApiSchema schema) MaxItems = schema.MaxItems ?? MaxItems; MinItems = schema.MinItems ?? MinItems; UniqueItems = schema.UniqueItems ?? UniqueItems; + if (schema is IOpenApiSchemaWithContainsProperties containsSchema) + { + Contains = containsSchema.Contains?.CreateShallowCopy(); + MaxContains = containsSchema.MaxContains ?? MaxContains; + MinContains = containsSchema.MinContains ?? MinContains; + } Properties = schema.Properties != null ? new Dictionary(schema.Properties) : null; PatternProperties = schema.PatternProperties != null ? new Dictionary(schema.PatternProperties) : null; MaxProperties = schema.MaxProperties ?? MaxProperties; @@ -630,6 +645,15 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer) writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (nodeWriter, s) => nodeWriter.WriteAny(s)); writer.WriteOptionalMap(OpenApiConstants.PatternProperties, PatternProperties, (w, s) => s.SerializeAsV31(w)); writer.WriteOptionalMap(OpenApiConstants.DependentRequired, DependentRequired, (w, s) => w.WriteValue(s)); + + // contains + writer.WriteOptionalObject(OpenApiConstants.Contains, Contains, (w, s) => s.SerializeAsV31(w)); + + // maxContains + writer.WriteProperty(OpenApiConstants.MaxContains, MaxContains); + + // minContains + writer.WriteProperty(OpenApiConstants.MinContains, MinContains); } internal void WriteAsItemsProperties(IOpenApiWriter writer) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs index 67eb79645..aef65205f 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs @@ -10,7 +10,7 @@ namespace Microsoft.OpenApi /// /// Schema reference object /// - public class OpenApiSchemaReference : BaseOpenApiReferenceHolder, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties, IOpenApiExtensible + public class OpenApiSchemaReference : BaseOpenApiReferenceHolder, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties, IOpenApiSchemaWithContainsProperties, IOpenApiExtensible { /// @@ -120,6 +120,12 @@ public bool WriteOnly /// public bool? UniqueItems { get => Target?.UniqueItems; } /// + public IOpenApiSchema? Contains { get => (Target as IOpenApiSchemaWithContainsProperties)?.Contains; } + /// + public uint? MaxContains { get => (Target as IOpenApiSchemaWithContainsProperties)?.MaxContains; } + /// + public uint? MinContains { get => (Target as IOpenApiSchemaWithContainsProperties)?.MinContains; } + /// public IDictionary? Properties { get => Target?.Properties; } /// public IDictionary? PatternProperties { get => Target?.PatternProperties; } diff --git a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt index 7dc5c5811..40ac9f0fc 100644 --- a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt +++ b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt @@ -1 +1,17 @@ #nullable enable +const Microsoft.OpenApi.OpenApiConstants.Contains = "contains" -> string! +const Microsoft.OpenApi.OpenApiConstants.MaxContains = "maxContains" -> string! +const Microsoft.OpenApi.OpenApiConstants.MinContains = "minContains" -> string! +Microsoft.OpenApi.IOpenApiSchemaWithContainsProperties +Microsoft.OpenApi.IOpenApiSchemaWithContainsProperties.Contains.get -> Microsoft.OpenApi.IOpenApiSchema? +Microsoft.OpenApi.IOpenApiSchemaWithContainsProperties.MaxContains.get -> uint? +Microsoft.OpenApi.IOpenApiSchemaWithContainsProperties.MinContains.get -> uint? +Microsoft.OpenApi.OpenApiSchema.Contains.get -> Microsoft.OpenApi.IOpenApiSchema? +Microsoft.OpenApi.OpenApiSchema.Contains.set -> void +Microsoft.OpenApi.OpenApiSchema.MaxContains.get -> uint? +Microsoft.OpenApi.OpenApiSchema.MaxContains.set -> void +Microsoft.OpenApi.OpenApiSchema.MinContains.get -> uint? +Microsoft.OpenApi.OpenApiSchema.MinContains.set -> void +Microsoft.OpenApi.OpenApiSchemaReference.Contains.get -> Microsoft.OpenApi.IOpenApiSchema? +Microsoft.OpenApi.OpenApiSchemaReference.MaxContains.get -> uint? +Microsoft.OpenApi.OpenApiSchemaReference.MinContains.get -> uint? diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs index 14deab765..6343ea4e7 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs @@ -144,6 +144,32 @@ internal static partial class OpenApiV31Deserializer } } }, + { + "contains", + (o, n, doc, c) => o.Contains = LoadSchema(n, doc, c) + }, + { + "maxContains", + (o, n, _, _) => + { + var maxContains = n.GetScalarValue(); + if (maxContains != null) + { + o.MaxContains = uint.Parse(maxContains, CultureInfo.InvariantCulture); + } + } + }, + { + "minContains", + (o, n, _, _) => + { + var minContains = n.GetScalarValue(); + if (minContains != null) + { + o.MinContains = uint.Parse(minContains, CultureInfo.InvariantCulture); + } + } + }, { "unevaluatedProperties", (o, n, t, c) => diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs index f0e07724f..ebcd05255 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs @@ -144,6 +144,32 @@ internal static partial class OpenApiV32Deserializer } } }, + { + "contains", + (o, n, doc, c) => o.Contains = LoadSchema(n, doc, c) + }, + { + "maxContains", + (o, n, _, _) => + { + var maxContains = n.GetScalarValue(); + if (maxContains != null) + { + o.MaxContains = uint.Parse(maxContains, CultureInfo.InvariantCulture); + } + } + }, + { + "minContains", + (o, n, _, _) => + { + var minContains = n.GetScalarValue(); + if (minContains != null) + { + o.MinContains = uint.Parse(minContains, CultureInfo.InvariantCulture); + } + } + }, { "unevaluatedProperties", (o, n, t, c) => diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs index ec66dcbb9..a6d7970ac 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs @@ -45,7 +45,13 @@ public async Task ParseBasicV31SchemaShouldSucceed() Items = new OpenApiSchema { Type = JsonSchemaType.String - } + }, + Contains = new OpenApiSchema + { + Type = JsonSchemaType.String + }, + MinContains = 1, + MaxContains = 5 }, ["vegetables"] = new OpenApiSchema { diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/jsonSchema.json b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/jsonSchema.json index 4a16ab4f5..4ee9fc8fa 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/jsonSchema.json +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/jsonSchema.json @@ -8,7 +8,12 @@ "type": "array", "items": { "type": "string" - } + }, + "contains": { + "type": "string" + }, + "minContains": 1, + "maxContains": 5 }, "vegetables": { "type": "array" diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs index 621cd156c..7d2997c9e 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs @@ -44,7 +44,13 @@ public async Task ParseBasicV32SchemaShouldSucceed() Items = new OpenApiSchema { Type = JsonSchemaType.String - } + }, + Contains = new OpenApiSchema + { + Type = JsonSchemaType.String + }, + MinContains = 1, + MaxContains = 5 }, ["vegetables"] = new OpenApiSchema { diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/jsonSchema.json b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/jsonSchema.json index dc55b72c2..c14fee6c1 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/jsonSchema.json +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/Samples/OpenApiSchema/jsonSchema.json @@ -8,7 +8,12 @@ "type": "array", "items": { "type": "string" - } + }, + "contains": { + "type": "string" + }, + "minContains": 1, + "maxContains": 5 }, "vegetables": { "type": "array" diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs index 40034eded..8acc771a2 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs @@ -523,6 +523,41 @@ public void OpenApiSchemaCopyConstructorWithUnevaluatedPropertiesSchemaSucceeds( Assert.Equal(100, baseSchema.UnevaluatedPropertiesSchema.MaxLength); } + [Fact] + public void OpenApiSchemaCopyConstructorWithContainsSucceeds() + { + var baseSchema = new OpenApiSchema + { + Type = JsonSchemaType.Array, + Contains = new OpenApiSchema + { + Type = JsonSchemaType.String, + MaxLength = 100 + }, + MinContains = 1, + MaxContains = 5 + }; + + var actualSchema = Assert.IsType(baseSchema.CreateShallowCopy()); + + // Verify scalar properties are copied + Assert.Equal(baseSchema.MinContains, actualSchema.MinContains); + Assert.Equal(baseSchema.MaxContains, actualSchema.MaxContains); + + // Verify schema property is copied + Assert.NotNull(actualSchema.Contains); + Assert.Equal(JsonSchemaType.String, actualSchema.Contains.Type); + Assert.Equal(100, actualSchema.Contains.MaxLength); + + // Verify it's a shallow copy (different object reference) + Assert.NotSame(baseSchema.Contains, actualSchema.Contains); + + // Verify that changing the copy doesn't affect the original + var actualContainsTyped = Assert.IsType(actualSchema.Contains); + actualContainsTyped.MaxLength = 200; + Assert.Equal(100, baseSchema.Contains.MaxLength); + } + public static TheoryData SchemaExamples() { return new() @@ -1164,6 +1199,75 @@ public async Task SerializeOneOfWithNullAndRefAsV3ShouldUseNullableAsync() Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema))); } + [Fact] + public async Task SerializeContainsKeywordsAsV31Works() + { + // Arrange + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Array, + Contains = new OpenApiSchema { Type = JsonSchemaType.String }, + MinContains = 1, + MaxContains = 5 + }; + + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = false }); + + // Act + schema.SerializeAsV31(writer); + await writer.FlushAsync(); + + var v31Schema = outputStringWriter.GetStringBuilder().ToString(); + + var expectedV31Schema = + """ + { + "type": "array", + "contains": { + "type": "string" + }, + "maxContains": 5, + "minContains": 1 + } + """; + + // Assert + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV31Schema), JsonNode.Parse(v31Schema))); + } + + [Fact] + public async Task SerializeContainsKeywordsAsV3DoesNotEmit() + { + // Arrange - contains/minContains/maxContains are JSON Schema 2020-12 keywords and have no equivalent in OpenAPI 3.0 + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Array, + Contains = new OpenApiSchema { Type = JsonSchemaType.String }, + MinContains = 1, + MaxContains = 5 + }; + + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = false }); + + // Act + schema.SerializeAsV3(writer); + await writer.FlushAsync(); + + var v3Schema = outputStringWriter.GetStringBuilder().ToString(); + + var expectedV3Schema = + """ + { + "type": "array" + } + """; + + // Assert + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema))); + } + // UnevaluatedProperties tests - similar to AdditionalProperties pattern [Fact] public async Task SerializeUnevaluatedPropertiesBooleanDefaultDoesNotEmit() From 8990afa1b619aaf573a538934e91c626575035c9 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 9 Jun 2026 14:52:01 -0400 Subject: [PATCH 2/5] chore(library): use schema contains constants in readers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Reader/V31/OpenApiSchemaDeserializer.cs | 6 +++--- .../Reader/V32/OpenApiSchemaDeserializer.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs index 7a6e9318c..962ad5283 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs @@ -149,11 +149,11 @@ internal static partial class OpenApiV31Deserializer } }, { - "contains", + OpenApiConstants.Contains, (o, n, doc, c) => o.Contains = LoadSchema(n, doc, c) }, { - "maxContains", + OpenApiConstants.MaxContains, (o, n, _, _) => { var maxContains = n.GetScalarValue(); @@ -164,7 +164,7 @@ internal static partial class OpenApiV31Deserializer } }, { - "minContains", + OpenApiConstants.MinContains, (o, n, _, _) => { var minContains = n.GetScalarValue(); diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs index aebfc3f9f..9d35aaf5a 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs @@ -149,11 +149,11 @@ internal static partial class OpenApiV32Deserializer } }, { - "contains", + OpenApiConstants.Contains, (o, n, doc, c) => o.Contains = LoadSchema(n, doc, c) }, { - "maxContains", + OpenApiConstants.MaxContains, (o, n, _, _) => { var maxContains = n.GetScalarValue(); @@ -164,7 +164,7 @@ internal static partial class OpenApiV32Deserializer } }, { - "minContains", + OpenApiConstants.MinContains, (o, n, _, _) => { var minContains = n.GetScalarValue(); From fe5ecbb3a88ded2fd24461f0f323f63a6fb3623a Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 9 Jun 2026 14:55:43 -0400 Subject: [PATCH 3/5] chore(library): serialize contains keywords as v3 extensions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Models/OpenApiConstants.cs | 15 +++++++++++++++ src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 3 +++ src/Microsoft.OpenApi/PublicAPI.Unshipped.txt | 3 +++ .../Models/OpenApiSchemaTests.cs | 18 +++++++++++++++--- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs index df5aa88f0..11f3926c5 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs @@ -895,6 +895,21 @@ public static class OpenApiConstants /// public const string ContentSchemaExtension = "x-jsonschema-contentSchema"; + /// + /// Extension: x-jsonschema-contains + /// + public const string ContainsExtension = "x-jsonschema-contains"; + + /// + /// Extension: x-jsonschema-maxContains + /// + public const string MaxContainsExtension = "x-jsonschema-maxContains"; + + /// + /// Extension: x-jsonschema-minContains + /// + public const string MinContainsExtension = "x-jsonschema-minContains"; + #region V2.0 /// diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 6a21630d8..225548d1f 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -713,6 +713,9 @@ private void WriteV3CompatibilityKeywords(IOpenApiWriter writer, Action string! const Microsoft.OpenApi.OpenApiConstants.ContentSchema = "contentSchema" -> string! const Microsoft.OpenApi.OpenApiConstants.ContentSchemaExtension = "x-jsonschema-contentSchema" -> string! +const Microsoft.OpenApi.OpenApiConstants.ContainsExtension = "x-jsonschema-contains" -> string! const Microsoft.OpenApi.OpenApiConstants.DependentSchemas = "dependentSchemas" -> string! const Microsoft.OpenApi.OpenApiConstants.DependentSchemasExtension = "x-jsonschema-dependentSchemas" -> string! const Microsoft.OpenApi.OpenApiConstants.Else = "else" -> string! const Microsoft.OpenApi.OpenApiConstants.ElseExtension = "x-jsonschema-else" -> string! const Microsoft.OpenApi.OpenApiConstants.If = "if" -> string! const Microsoft.OpenApi.OpenApiConstants.IfExtension = "x-jsonschema-if" -> string! +const Microsoft.OpenApi.OpenApiConstants.MaxContainsExtension = "x-jsonschema-maxContains" -> string! +const Microsoft.OpenApi.OpenApiConstants.MinContainsExtension = "x-jsonschema-minContains" -> string! const Microsoft.OpenApi.OpenApiConstants.PropertyNames = "propertyNames" -> string! const Microsoft.OpenApi.OpenApiConstants.PropertyNamesExtension = "x-jsonschema-propertyNames" -> string! const Microsoft.OpenApi.OpenApiConstants.Then = "then" -> string! diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs index 5f063f2b1..22d0b7d67 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs @@ -1279,9 +1279,8 @@ public async Task SerializeContainsKeywordsAsV31Works() } [Fact] - public async Task SerializeContainsKeywordsAsV3DoesNotEmit() + public async Task SerializeContainsKeywordsAsV3EmitsCompatibilityExtensions() { - // Arrange - contains/minContains/maxContains are JSON Schema 2020-12 keywords and have no equivalent in OpenAPI 3.0 var schema = new OpenApiSchema { Type = JsonSchemaType.Array, @@ -1302,7 +1301,12 @@ public async Task SerializeContainsKeywordsAsV3DoesNotEmit() var expectedV3Schema = """ { - "type": "array" + "type": "array", + "x-jsonschema-contains": { + "type": "string" + }, + "x-jsonschema-maxContains": 5, + "x-jsonschema-minContains": 1 } """; @@ -1521,6 +1525,11 @@ public async Task SerializeMissingPropertiesEmitsOaiExtensionsInV3() "x-jsonschema-contentSchema": { "type": "array" }, + "x-jsonschema-contains": { + "type": "string" + }, + "x-jsonschema-maxContains": 3, + "x-jsonschema-minContains": 1, "x-jsonschema-propertyNames": { "pattern": "^[a-z]+$" }, @@ -1549,6 +1558,9 @@ public async Task SerializeMissingPropertiesEmitsOaiExtensionsInV3() ContentEncoding = "base64", ContentMediaType = "application/jwt", ContentSchema = new OpenApiSchema { Type = JsonSchemaType.Array }, + Contains = new OpenApiSchema { Type = JsonSchemaType.String }, + MaxContains = 3, + MinContains = 1, PropertyNames = new OpenApiSchema { Pattern = "^[a-z]+$" }, DependentSchemas = new Dictionary { From 7927d30739411b54354d97619a56ad57c7ac3088 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 9 Jun 2026 15:05:45 -0400 Subject: [PATCH 4/5] chore(benchmark): refresh performance reports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../performance.Descriptions-report-github.md | 16 ++--- .../performance.Descriptions-report.csv | 12 ++-- .../performance.Descriptions-report.html | 16 ++--- .../performance.Descriptions-report.json | 2 +- .../performance.EmptyModels-report-github.md | 56 +++++++++--------- .../performance.EmptyModels-report.csv | 56 +++++++++--------- .../performance.EmptyModels-report.html | 58 +++++++++---------- .../performance.EmptyModels-report.json | 2 +- 8 files changed, 109 insertions(+), 109 deletions(-) diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md index 1b2eb289d..a7b5f3181 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md @@ -10,11 +10,11 @@ Job=ShortRun IterationCount=3 LaunchCount=1 WarmupCount=3 ``` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | -|------------- |-------------:|--------------:|------------:|-----------:|-----------:|----------:|-------------:| -| PetStoreYaml | 371.5 μs | 35.60 μs | 1.95 μs | 74.2188 | 11.7188 | - | 307.17 KB | -| PetStoreJson | 155.7 μs | 10.23 μs | 0.56 μs | 41.0156 | 6.8359 | - | 169.31 KB | -| GHESYaml | 771,340.7 μs | 72,493.09 μs | 3,973.59 μs | 44000.0000 | 18000.0000 | 3000.0000 | 252535.98 KB | -| GHESJson | 308,100.8 μs | 132,615.87 μs | 7,269.12 μs | 17000.0000 | 9000.0000 | 2000.0000 | 109706.91 KB | -| GHESNextYaml | 999,238.5 μs | 116,421.98 μs | 6,381.48 μs | 80000.0000 | 20000.0000 | 3000.0000 | 446197.67 KB | -| GHESNextJson | 565,582.8 μs | 54,146.09 μs | 2,967.93 μs | 52000.0000 | 14000.0000 | 3000.0000 | 307956.73 KB | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|------------- |---------------:|--------------:|-------------:|-----------:|-----------:|----------:|-------------:| +| PetStoreYaml | 371.7 μs | 35.71 μs | 1.96 μs | 74.2188 | 15.6250 | - | 307.59 KB | +| PetStoreJson | 155.8 μs | 27.95 μs | 1.53 μs | 41.0156 | 2.9297 | - | 169.74 KB | +| GHESYaml | 820,515.0 μs | 271,578.81 μs | 14,886.15 μs | 45000.0000 | 18000.0000 | 3000.0000 | 253340.42 KB | +| GHESJson | 302,067.9 μs | 133,906.46 μs | 7,339.86 μs | 18000.0000 | 10000.0000 | 2000.0000 | 110511.77 KB | +| GHESNextYaml | 1,023,253.0 μs | 242,683.77 μs | 13,302.32 μs | 80000.0000 | 19000.0000 | 3000.0000 | 447044.99 KB | +| GHESNextJson | 577,121.9 μs | 340,214.97 μs | 18,648.33 μs | 52000.0000 | 13000.0000 | 3000.0000 | 308806.54 KB | diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv index 655f0f4b4..937693354 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv @@ -1,7 +1,7 @@ Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,LargeAddressAware,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,Mean,Error,StdDev,Gen0,Gen1,Gen2,Allocated -PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,371.5 μs,35.60 μs,1.95 μs,74.2188,11.7188,0.0000,307.17 KB -PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,155.7 μs,10.23 μs,0.56 μs,41.0156,6.8359,0.0000,169.31 KB -GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"771,340.7 μs","72,493.09 μs","3,973.59 μs",44000.0000,18000.0000,3000.0000,252535.98 KB -GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"308,100.8 μs","132,615.87 μs","7,269.12 μs",17000.0000,9000.0000,2000.0000,109706.91 KB -GHESNextYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"999,238.5 μs","116,421.98 μs","6,381.48 μs",80000.0000,20000.0000,3000.0000,446197.67 KB -GHESNextJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"565,582.8 μs","54,146.09 μs","2,967.93 μs",52000.0000,14000.0000,3000.0000,307956.73 KB +PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,371.7 μs,35.71 μs,1.96 μs,74.2188,15.6250,0.0000,307.59 KB +PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,155.8 μs,27.95 μs,1.53 μs,41.0156,2.9297,0.0000,169.74 KB +GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"820,515.0 μs","271,578.81 μs","14,886.15 μs",45000.0000,18000.0000,3000.0000,253340.42 KB +GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"302,067.9 μs","133,906.46 μs","7,339.86 μs",18000.0000,10000.0000,2000.0000,110511.77 KB +GHESNextYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"1,023,253.0 μs","242,683.77 μs","13,302.32 μs",80000.0000,19000.0000,3000.0000,447044.99 KB +GHESNextJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"577,121.9 μs","340,214.97 μs","18,648.33 μs",52000.0000,13000.0000,3000.0000,308806.54 KB diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html index b45bbfcc7..9d107848f 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html @@ -2,7 +2,7 @@ -performance.Descriptions-20260609-124950 +performance.Descriptions-20260609-145620