Skip to content

Commit fadb422

Browse files
Poltuubaywet
authored andcommitted
feat: add contains/minContains/maxContains members
1 parent 5360b4a commit fadb422

9 files changed

Lines changed: 237 additions & 4 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace Microsoft.OpenApi;
2+
3+
/// <summary>
4+
/// Compatibility interface for the JSON Schema 2020-12 "contains" keywords support.
5+
/// This interface provides access to the Contains, MaxContains and MinContains properties, which were
6+
/// missed in the initial release of the IOpenApiSchema interface.
7+
///
8+
/// This is a temporary compatibility solution. In the next major version this interface should be
9+
/// merged into IOpenApiSchema.
10+
/// </summary>
11+
public interface IOpenApiSchemaWithContainsProperties
12+
{
13+
/// <summary>
14+
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-core#name-contains
15+
/// An array instance is valid against "contains" if at least one of its elements is valid against this schema.
16+
/// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema.
17+
/// </summary>
18+
IOpenApiSchema? Contains { get; }
19+
20+
/// <summary>
21+
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
22+
/// The number of elements matching the "contains" schema MUST be less than or equal to this value.
23+
/// </summary>
24+
uint? MaxContains { get; }
25+
26+
/// <summary>
27+
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
28+
/// The number of elements matching the "contains" schema MUST be greater than or equal to this value.
29+
/// </summary>
30+
uint? MinContains { get; }
31+
}

src/Microsoft.OpenApi/Models/OpenApiConstants.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,21 @@ public static class OpenApiConstants
450450
/// </summary>
451451
public const string UniqueItems = "uniqueItems";
452452

453+
/// <summary>
454+
/// Field: Contains
455+
/// </summary>
456+
public const string Contains = "contains";
457+
458+
/// <summary>
459+
/// Field: MaxContains
460+
/// </summary>
461+
public const string MaxContains = "maxContains";
462+
463+
/// <summary>
464+
/// Field: MinContains
465+
/// </summary>
466+
public const string MinContains = "minContains";
467+
453468
/// <summary>
454469
/// Field: MaxProperties
455470
/// </summary>

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ namespace Microsoft.OpenApi
1919
/// - Serialization: To produce something functionally equivalent to boolean schemas, create an empty <see cref="OpenApiSchema"/>
2020
/// for "true" behavior, or create a schema with only <see cref="Not"/> set to an empty schema for "false" behavior.
2121
/// </summary>
22-
public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaMissingProperties, IOpenApiSchemaWithUnevaluatedProperties, IMetadataContainer
22+
public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaMissingProperties, IOpenApiSchemaWithUnevaluatedProperties, IOpenApiSchemaWithContainsProperties, IMetadataContainer
2323
{
2424
/// <inheritdoc />
2525
public string? Title { get; set; }
@@ -211,6 +211,15 @@ public string? Minimum
211211
/// <inheritdoc />
212212
public bool? UniqueItems { get; set; }
213213

214+
/// <inheritdoc />
215+
public IOpenApiSchema? Contains { get; set; }
216+
217+
/// <inheritdoc />
218+
public uint? MaxContains { get; set; }
219+
220+
/// <inheritdoc />
221+
public uint? MinContains { get; set; }
222+
214223
/// <inheritdoc />
215224
public IDictionary<string, IOpenApiSchema>? Properties { get; set; }
216225

@@ -361,6 +370,12 @@ internal OpenApiSchema(IOpenApiSchema schema)
361370
MaxItems = schema.MaxItems ?? MaxItems;
362371
MinItems = schema.MinItems ?? MinItems;
363372
UniqueItems = schema.UniqueItems ?? UniqueItems;
373+
if (schema is IOpenApiSchemaWithContainsProperties containsSchema)
374+
{
375+
Contains = containsSchema.Contains?.CreateShallowCopy();
376+
MaxContains = containsSchema.MaxContains ?? MaxContains;
377+
MinContains = containsSchema.MinContains ?? MinContains;
378+
}
364379
Properties = schema.Properties != null ? new Dictionary<string, IOpenApiSchema>(schema.Properties) : null;
365380
PatternProperties = schema.PatternProperties != null ? new Dictionary<string, IOpenApiSchema>(schema.PatternProperties) : null;
366381
MaxProperties = schema.MaxProperties ?? MaxProperties;
@@ -679,6 +694,15 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer, Action<IOpenApiWrit
679694
writer.WriteOptionalObject(OpenApiConstants.If, If, callback);
680695
writer.WriteOptionalObject(OpenApiConstants.Then, Then, callback);
681696
writer.WriteOptionalObject(OpenApiConstants.Else, Else, callback);
697+
698+
// contains
699+
writer.WriteOptionalObject(OpenApiConstants.Contains, Contains, (w, s) => s.SerializeAsV31(w));
700+
701+
// maxContains
702+
writer.WriteProperty(OpenApiConstants.MaxContains, MaxContains);
703+
704+
// minContains
705+
writer.WriteProperty(OpenApiConstants.MinContains, MinContains);
682706
}
683707

684708
private void WriteV3CompatibilityKeywords(IOpenApiWriter writer, Action<IOpenApiWriter, IOpenApiSerializable> callback)

src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Microsoft.OpenApi
1111
/// <summary>
1212
/// Schema reference object
1313
/// </summary>
14-
public class OpenApiSchemaReference : BaseOpenApiReferenceHolder<OpenApiSchema, IOpenApiSchema, JsonSchemaReference>, IOpenApiSchema, IOpenApiSchemaMissingProperties, IOpenApiSchemaWithUnevaluatedProperties, IOpenApiExtensible
14+
public class OpenApiSchemaReference : BaseOpenApiReferenceHolder<OpenApiSchema, IOpenApiSchema, JsonSchemaReference>, IOpenApiSchema, IOpenApiSchemaMissingProperties, IOpenApiSchemaWithUnevaluatedProperties, IOpenApiSchemaWithContainsProperties, IOpenApiExtensible
1515
{
1616

1717
/// <summary>
@@ -123,6 +123,12 @@ public bool WriteOnly
123123
/// <inheritdoc/>
124124
public bool? UniqueItems { get => Target?.UniqueItems; }
125125
/// <inheritdoc/>
126+
public IOpenApiSchema? Contains { get => (Target as IOpenApiSchemaWithContainsProperties)?.Contains; }
127+
/// <inheritdoc/>
128+
public uint? MaxContains { get => (Target as IOpenApiSchemaWithContainsProperties)?.MaxContains; }
129+
/// <inheritdoc/>
130+
public uint? MinContains { get => (Target as IOpenApiSchemaWithContainsProperties)?.MinContains; }
131+
/// <inheritdoc/>
126132
public IDictionary<string, IOpenApiSchema>? Properties { get => Target?.Properties; }
127133
/// <inheritdoc/>
128134
public IDictionary<string, IOpenApiSchema>? PatternProperties { get => Target?.PatternProperties; }

src/Microsoft.OpenApi/PublicAPI.Unshipped.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,19 @@ Microsoft.OpenApi.OpenApiSchemaReference.Else.get -> Microsoft.OpenApi.IOpenApiS
5656
Microsoft.OpenApi.OpenApiSchemaReference.If.get -> Microsoft.OpenApi.IOpenApiSchema?
5757
Microsoft.OpenApi.OpenApiSchemaReference.PropertyNames.get -> Microsoft.OpenApi.IOpenApiSchema?
5858
Microsoft.OpenApi.OpenApiSchemaReference.Then.get -> Microsoft.OpenApi.IOpenApiSchema?
59+
const Microsoft.OpenApi.OpenApiConstants.Contains = "contains" -> string!
60+
const Microsoft.OpenApi.OpenApiConstants.MaxContains = "maxContains" -> string!
61+
const Microsoft.OpenApi.OpenApiConstants.MinContains = "minContains" -> string!
62+
Microsoft.OpenApi.IOpenApiSchemaWithContainsProperties
63+
Microsoft.OpenApi.IOpenApiSchemaWithContainsProperties.Contains.get -> Microsoft.OpenApi.IOpenApiSchema?
64+
Microsoft.OpenApi.IOpenApiSchemaWithContainsProperties.MaxContains.get -> uint?
65+
Microsoft.OpenApi.IOpenApiSchemaWithContainsProperties.MinContains.get -> uint?
66+
Microsoft.OpenApi.OpenApiSchema.Contains.get -> Microsoft.OpenApi.IOpenApiSchema?
67+
Microsoft.OpenApi.OpenApiSchema.Contains.set -> void
68+
Microsoft.OpenApi.OpenApiSchema.MaxContains.get -> uint?
69+
Microsoft.OpenApi.OpenApiSchema.MaxContains.set -> void
70+
Microsoft.OpenApi.OpenApiSchema.MinContains.get -> uint?
71+
Microsoft.OpenApi.OpenApiSchema.MinContains.set -> void
72+
Microsoft.OpenApi.OpenApiSchemaReference.Contains.get -> Microsoft.OpenApi.IOpenApiSchema?
73+
Microsoft.OpenApi.OpenApiSchemaReference.MaxContains.get -> uint?
74+
Microsoft.OpenApi.OpenApiSchemaReference.MinContains.get -> uint?

src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,32 @@ internal static partial class OpenApiV31Deserializer
148148
}
149149
}
150150
},
151+
{
152+
"contains",
153+
(o, n, doc, c) => o.Contains = LoadSchema(n, doc, c)
154+
},
155+
{
156+
"maxContains",
157+
(o, n, _, _) =>
158+
{
159+
var maxContains = n.GetScalarValue();
160+
if (maxContains != null)
161+
{
162+
o.MaxContains = uint.Parse(maxContains, CultureInfo.InvariantCulture);
163+
}
164+
}
165+
},
166+
{
167+
"minContains",
168+
(o, n, _, _) =>
169+
{
170+
var minContains = n.GetScalarValue();
171+
if (minContains != null)
172+
{
173+
o.MinContains = uint.Parse(minContains, CultureInfo.InvariantCulture);
174+
}
175+
}
176+
},
151177
{
152178
"unevaluatedProperties",
153179
(o, n, t, c) =>

test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,13 @@ public async Task ParseBasicV31SchemaShouldSucceed()
4545
Items = new OpenApiSchema
4646
{
4747
Type = JsonSchemaType.String
48-
}
48+
},
49+
Contains = new OpenApiSchema
50+
{
51+
Type = JsonSchemaType.String
52+
},
53+
MinContains = 1,
54+
MaxContains = 5
4955
},
5056
["vegetables"] = new OpenApiSchema
5157
{

test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/jsonSchema.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
"type": "array",
99
"items": {
1010
"type": "string"
11-
}
11+
},
12+
"contains": {
13+
"type": "string"
14+
},
15+
"minContains": 1,
16+
"maxContains": 5
1217
},
1318
"vegetables": {
1419
"type": "array"

test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,41 @@ public void OpenApiSchemaCopyConstructorWithMissingPropertiesSucceeds()
564564
Assert.NotSame(baseSchema.Else, actualMissingProperties.Else);
565565
}
566566

567+
[Fact]
568+
public void OpenApiSchemaCopyConstructorWithContainsSucceeds()
569+
{
570+
var baseSchema = new OpenApiSchema
571+
{
572+
Type = JsonSchemaType.Array,
573+
Contains = new OpenApiSchema
574+
{
575+
Type = JsonSchemaType.String,
576+
MaxLength = 100
577+
},
578+
MinContains = 1,
579+
MaxContains = 5
580+
};
581+
582+
var actualSchema = Assert.IsType<OpenApiSchema>(baseSchema.CreateShallowCopy());
583+
584+
// Verify scalar properties are copied
585+
Assert.Equal(baseSchema.MinContains, actualSchema.MinContains);
586+
Assert.Equal(baseSchema.MaxContains, actualSchema.MaxContains);
587+
588+
// Verify schema property is copied
589+
Assert.NotNull(actualSchema.Contains);
590+
Assert.Equal(JsonSchemaType.String, actualSchema.Contains.Type);
591+
Assert.Equal(100, actualSchema.Contains.MaxLength);
592+
593+
// Verify it's a shallow copy (different object reference)
594+
Assert.NotSame(baseSchema.Contains, actualSchema.Contains);
595+
596+
// Verify that changing the copy doesn't affect the original
597+
var actualContainsTyped = Assert.IsType<OpenApiSchema>(actualSchema.Contains);
598+
actualContainsTyped.MaxLength = 200;
599+
Assert.Equal(100, baseSchema.Contains.MaxLength);
600+
}
601+
567602
public static TheoryData<JsonNode> SchemaExamples()
568603
{
569604
return new()
@@ -1202,6 +1237,75 @@ public async Task SerializeOneOfWithNullAndRefAsV3ShouldUseNullableAsync()
12021237
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema)));
12031238
}
12041239

1240+
[Fact]
1241+
public async Task SerializeContainsKeywordsAsV31Works()
1242+
{
1243+
// Arrange
1244+
var schema = new OpenApiSchema
1245+
{
1246+
Type = JsonSchemaType.Array,
1247+
Contains = new OpenApiSchema { Type = JsonSchemaType.String },
1248+
MinContains = 1,
1249+
MaxContains = 5
1250+
};
1251+
1252+
var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
1253+
var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = false });
1254+
1255+
// Act
1256+
schema.SerializeAsV31(writer);
1257+
await writer.FlushAsync();
1258+
1259+
var v31Schema = outputStringWriter.GetStringBuilder().ToString();
1260+
1261+
var expectedV31Schema =
1262+
"""
1263+
{
1264+
"type": "array",
1265+
"contains": {
1266+
"type": "string"
1267+
},
1268+
"maxContains": 5,
1269+
"minContains": 1
1270+
}
1271+
""";
1272+
1273+
// Assert
1274+
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV31Schema), JsonNode.Parse(v31Schema)));
1275+
}
1276+
1277+
[Fact]
1278+
public async Task SerializeContainsKeywordsAsV3DoesNotEmit()
1279+
{
1280+
// Arrange - contains/minContains/maxContains are JSON Schema 2020-12 keywords and have no equivalent in OpenAPI 3.0
1281+
var schema = new OpenApiSchema
1282+
{
1283+
Type = JsonSchemaType.Array,
1284+
Contains = new OpenApiSchema { Type = JsonSchemaType.String },
1285+
MinContains = 1,
1286+
MaxContains = 5
1287+
};
1288+
1289+
var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
1290+
var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = false });
1291+
1292+
// Act
1293+
schema.SerializeAsV3(writer);
1294+
await writer.FlushAsync();
1295+
1296+
var v3Schema = outputStringWriter.GetStringBuilder().ToString();
1297+
1298+
var expectedV3Schema =
1299+
"""
1300+
{
1301+
"type": "array"
1302+
}
1303+
""";
1304+
1305+
// Assert
1306+
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema)));
1307+
}
1308+
12051309
// UnevaluatedProperties tests - similar to AdditionalProperties pattern
12061310
[Fact]
12071311
public async Task SerializeUnevaluatedPropertiesBooleanDefaultDoesNotEmit()

0 commit comments

Comments
 (0)