Skip to content

Commit af42279

Browse files
Copilotbaywet
andcommitted
Add support for mediaType itemSchema property (OAI 3.2.0)
Co-authored-by: baywet <7905502+baywet@users.noreply.github.com>
1 parent 093737e commit af42279

File tree

11 files changed

+294
-2
lines changed

11 files changed

+294
-2
lines changed

src/Microsoft.OpenApi/Models/OpenApiConstants.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,11 @@ public static class OpenApiConstants
295295
/// </summary>
296296
public const string Schema = "schema";
297297

298+
/// <summary>
299+
/// Field: ItemSchema
300+
/// </summary>
301+
public const string ItemSchema = "itemSchema";
302+
298303
/// <summary>
299304
/// Field: Schemas
300305
/// </summary>

src/Microsoft.OpenApi/Models/OpenApiMediaType.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ public class OpenApiMediaType : IOpenApiSerializable, IOpenApiExtensible
1818
/// </summary>
1919
public IOpenApiSchema? Schema { get; set; }
2020

21+
/// <summary>
22+
/// The schema defining the type used for the items in an array media type.
23+
/// This property is only applicable for OAS 3.2.0 and later.
24+
/// </summary>
25+
public IOpenApiSchema? ItemSchema { get; set; }
26+
2127
/// <summary>
2228
/// Example of the media type.
2329
/// The example object SHOULD be in the correct format as specified by the media type.
@@ -54,6 +60,7 @@ public OpenApiMediaType() { }
5460
public OpenApiMediaType(OpenApiMediaType? mediaType)
5561
{
5662
Schema = mediaType?.Schema?.CreateShallowCopy();
63+
ItemSchema = mediaType?.ItemSchema?.CreateShallowCopy();
5764
Example = mediaType?.Example != null ? JsonNodeCloneHelper.Clone(mediaType.Example) : null;
5865
Examples = mediaType?.Examples != null ? new Dictionary<string, IOpenApiExample>(mediaType.Examples) : null;
5966
Encoding = mediaType?.Encoding != null ? new Dictionary<string, OpenApiEncoding>(mediaType.Encoding) : null;
@@ -96,6 +103,18 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
96103
// schema
97104
writer.WriteOptionalObject(OpenApiConstants.Schema, Schema, callback);
98105

106+
// itemSchema - only for v3.2+
107+
if (version >= OpenApiSpecVersion.OpenApi3_2)
108+
{
109+
writer.WriteOptionalObject(OpenApiConstants.ItemSchema, ItemSchema, callback);
110+
}
111+
else if (version < OpenApiSpecVersion.OpenApi3_2 && ItemSchema != null)
112+
{
113+
// For v3.1 and earlier, serialize as x-oai-itemSchema extension
114+
writer.WritePropertyName(OpenApiConstants.ExtensionFieldNamePrefix + "oai-" + OpenApiConstants.ItemSchema);
115+
callback(writer, ItemSchema);
116+
}
117+
99118
// example
100119
writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e));
101120

src/Microsoft.OpenApi/Reader/V3/OpenApiMediaTypeDeserializer.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,18 @@ internal static partial class OpenApiV3Deserializer
3535
private static readonly PatternFieldMap<OpenApiMediaType> _mediaTypePatternFields =
3636
new()
3737
{
38-
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p,n))}
38+
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, t) =>
39+
{
40+
// Handle x-oai-itemSchema as ItemSchema property for forward compatibility
41+
if (p.Equals("x-oai-itemSchema", StringComparison.OrdinalIgnoreCase))
42+
{
43+
o.ItemSchema = LoadSchema(n, t);
44+
}
45+
else
46+
{
47+
o.AddExtension(p, LoadExtension(p, n));
48+
}
49+
}}
3950
};
4051

4152
private static readonly AnyFieldMap<OpenApiMediaType> _mediaTypeAnyFields = new()

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,18 @@ internal static partial class OpenApiV31Deserializer
4040
private static readonly PatternFieldMap<OpenApiMediaType> _mediaTypePatternFields =
4141
new()
4242
{
43-
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p,n))}
43+
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, t) =>
44+
{
45+
// Handle x-oai-itemSchema as ItemSchema property for forward compatibility
46+
if (p.Equals("x-oai-itemSchema", StringComparison.OrdinalIgnoreCase))
47+
{
48+
o.ItemSchema = LoadSchema(n, t);
49+
}
50+
else
51+
{
52+
o.AddExtension(p, LoadExtension(p, n));
53+
}
54+
}}
4455
};
4556

4657
private static readonly AnyFieldMap<OpenApiMediaType> _mediaTypeAnyFields = new AnyFieldMap<OpenApiMediaType>

src/Microsoft.OpenApi/Reader/V32/OpenApiMediaTypeDeserializer.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ internal static partial class OpenApiV32Deserializer
1717
o.Schema = LoadSchema(n, t);
1818
}
1919
},
20+
{
21+
OpenApiConstants.ItemSchema, (o, n, t) =>
22+
{
23+
o.ItemSchema = LoadSchema(n, t);
24+
}
25+
},
2026
{
2127
OpenApiConstants.Examples, (o, n, t) =>
2228
{
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using System.IO;
5+
using System.Threading.Tasks;
6+
using FluentAssertions;
7+
using Microsoft.OpenApi.Reader;
8+
using Microsoft.OpenApi.Tests;
9+
using Xunit;
10+
11+
namespace Microsoft.OpenApi.Readers.Tests.V31Tests
12+
{
13+
[Collection("DefaultSettings")]
14+
public class OpenApiMediaTypeTests
15+
{
16+
private const string SampleFolderPath = "V31Tests/Samples/OpenApiMediaType/";
17+
18+
[Fact]
19+
public async Task ParseMediaTypeWithXOaiItemSchemaShouldSucceed()
20+
{
21+
// Act
22+
var mediaType = await OpenApiModelFactory.LoadAsync<OpenApiMediaType>(
23+
Path.Combine(SampleFolderPath, "mediaTypeWithXOaiItemSchema.yaml"),
24+
OpenApiSpecVersion.OpenApi3_1,
25+
new(),
26+
SettingsFixture.ReaderSettings);
27+
28+
// Assert
29+
mediaType.Should().BeEquivalentTo(
30+
new OpenApiMediaType
31+
{
32+
Schema = new OpenApiSchema
33+
{
34+
Type = JsonSchemaType.Array
35+
},
36+
ItemSchema = new OpenApiSchema
37+
{
38+
Type = JsonSchemaType.String,
39+
MaxLength = 100
40+
}
41+
},
42+
options => options.IgnoringCyclicReferences());
43+
}
44+
}
45+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
schema:
2+
type: array
3+
x-oai-itemSchema:
4+
type: string
5+
maxLength: 100
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using System.IO;
5+
using System.Threading.Tasks;
6+
using FluentAssertions;
7+
using Microsoft.OpenApi.Reader;
8+
using Microsoft.OpenApi.Tests;
9+
using Xunit;
10+
11+
namespace Microsoft.OpenApi.Readers.Tests.V32Tests
12+
{
13+
[Collection("DefaultSettings")]
14+
public class OpenApiMediaTypeTests
15+
{
16+
private const string SampleFolderPath = "V32Tests/Samples/OpenApiMediaType/";
17+
18+
[Fact]
19+
public async Task ParseMediaTypeWithItemSchemaShouldSucceed()
20+
{
21+
// Act
22+
var mediaType = await OpenApiModelFactory.LoadAsync<OpenApiMediaType>(
23+
Path.Combine(SampleFolderPath, "mediaTypeWithItemSchema.yaml"),
24+
OpenApiSpecVersion.OpenApi3_2,
25+
new(),
26+
SettingsFixture.ReaderSettings);
27+
28+
// Assert
29+
mediaType.Should().BeEquivalentTo(
30+
new OpenApiMediaType
31+
{
32+
Schema = new OpenApiSchema
33+
{
34+
Type = JsonSchemaType.Array
35+
},
36+
ItemSchema = new OpenApiSchema
37+
{
38+
Type = JsonSchemaType.String,
39+
MaxLength = 100
40+
}
41+
},
42+
options => options.IgnoringCyclicReferences());
43+
}
44+
}
45+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
schema:
2+
type: array
3+
itemSchema:
4+
type: string
5+
maxLength: 100

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

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,5 +443,143 @@ public void MediaTypeCopyConstructorWorks()
443443
Assert.Empty(clone.Extensions);
444444
Assert.Null(MediaTypeWithObjectExamples.Example);
445445
}
446+
447+
[Fact]
448+
public async Task SerializeMediaTypeWithItemSchemaAsV32JsonWorks()
449+
{
450+
// Arrange
451+
var mediaType = new OpenApiMediaType
452+
{
453+
Schema = new OpenApiSchema
454+
{
455+
Type = JsonSchemaType.Array
456+
},
457+
ItemSchema = new OpenApiSchema
458+
{
459+
Type = JsonSchemaType.String,
460+
MaxLength = 100
461+
}
462+
};
463+
464+
var expected =
465+
"""
466+
{
467+
"schema": {
468+
"type": "array"
469+
},
470+
"itemSchema": {
471+
"maxLength": 100,
472+
"type": "string"
473+
}
474+
}
475+
""";
476+
477+
// Act
478+
var actual = await mediaType.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_2);
479+
480+
// Assert
481+
actual = actual.MakeLineBreaksEnvironmentNeutral();
482+
expected = expected.MakeLineBreaksEnvironmentNeutral();
483+
Assert.Equal(expected, actual);
484+
}
485+
486+
[Fact]
487+
public async Task SerializeMediaTypeWithItemSchemaAsV31JsonWorks()
488+
{
489+
// Arrange
490+
var mediaType = new OpenApiMediaType
491+
{
492+
Schema = new OpenApiSchema
493+
{
494+
Type = JsonSchemaType.Array
495+
},
496+
ItemSchema = new OpenApiSchema
497+
{
498+
Type = JsonSchemaType.String,
499+
MaxLength = 100
500+
}
501+
};
502+
503+
var expected =
504+
"""
505+
{
506+
"schema": {
507+
"type": "array"
508+
},
509+
"x-oai-itemSchema": {
510+
"maxLength": 100,
511+
"type": "string"
512+
}
513+
}
514+
""";
515+
516+
// Act
517+
var actual = await mediaType.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1);
518+
519+
// Assert
520+
actual = actual.MakeLineBreaksEnvironmentNeutral();
521+
expected = expected.MakeLineBreaksEnvironmentNeutral();
522+
Assert.Equal(expected, actual);
523+
}
524+
525+
[Fact]
526+
public async Task SerializeMediaTypeWithItemSchemaAsV3JsonWorks()
527+
{
528+
// Arrange
529+
var mediaType = new OpenApiMediaType
530+
{
531+
Schema = new OpenApiSchema
532+
{
533+
Type = JsonSchemaType.Array
534+
},
535+
ItemSchema = new OpenApiSchema
536+
{
537+
Type = JsonSchemaType.String,
538+
MaxLength = 100
539+
}
540+
};
541+
542+
var expected =
543+
"""
544+
{
545+
"schema": {
546+
"type": "array"
547+
},
548+
"x-oai-itemSchema": {
549+
"maxLength": 100,
550+
"type": "string"
551+
}
552+
}
553+
""";
554+
555+
// Act
556+
var actual = await mediaType.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
557+
558+
// Assert
559+
actual = actual.MakeLineBreaksEnvironmentNeutral();
560+
expected = expected.MakeLineBreaksEnvironmentNeutral();
561+
Assert.Equal(expected, actual);
562+
}
563+
564+
[Fact]
565+
public void MediaTypeCopyConstructorCopiesItemSchema()
566+
{
567+
// Arrange
568+
var original = new OpenApiMediaType
569+
{
570+
ItemSchema = new OpenApiSchema
571+
{
572+
Type = JsonSchemaType.String
573+
}
574+
};
575+
576+
// Act
577+
var clone = new OpenApiMediaType(original);
578+
579+
// Assert
580+
Assert.NotNull(clone.ItemSchema);
581+
Assert.Equal(JsonSchemaType.String, clone.ItemSchema.Type);
582+
Assert.NotSame(original.ItemSchema, clone.ItemSchema);
583+
}
446584
}
447585
}

0 commit comments

Comments
 (0)