diff --git a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs index db8aa1d62..3cfc9f357 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs @@ -755,6 +755,11 @@ public static class OpenApiConstants /// public const string ExamplesExtension = "x-examples"; + /// + /// Extension: x-jsonschema-examples + /// + public const string JsonSchemaExamplesExtension = "x-jsonschema-examples"; + /// /// Field: version3_0_0 /// diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 814dc1923..b94ab74eb 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -615,7 +615,10 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, callback); // example - writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e)); + writer.WriteOptionalObject( + OpenApiConstants.Example, + version < OpenApiSpecVersion.OpenApi3_1 ? GetCompatibilityExample() : Example, + (w, e) => w.WriteAny(e)); // deprecated writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated, false); @@ -741,6 +744,7 @@ private void WriteV3CompatibilityKeywords(IOpenApiWriter writer, Action nodeWriter.WriteAny(s)); } internal void WriteAsItemsProperties(IOpenApiWriter writer) @@ -954,7 +958,7 @@ private void SerializeAsV2( writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, (w, s) => s.SerializeAsV2(w)); // example - writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e)); + writer.WriteOptionalObject(OpenApiConstants.Example, GetCompatibilityExample(), (w, e) => w.WriteAny(e)); // x-nullable extension SerializeNullable(writer, OpenApiSpecVersion.OpenApi2_0); @@ -985,6 +989,8 @@ private void SerializeAsV2( writer.WriteOptionalMap(OpenApiConstants.PatternPropertiesExtension, PatternProperties, (w, s) => s.SerializeAsV2(w)); } + writer.WriteOptionalCollection(OpenApiConstants.JsonSchemaExamplesExtension, GetCompatibilityExamplesExtension(), (nodeWriter, s) => nodeWriter.WriteAny(s)); + // extensions writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0); @@ -1019,6 +1025,26 @@ private bool TrySerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion return false; } + private JsonNode? GetCompatibilityExample() + { + return Example ?? Examples?.FirstOrDefault(); + } + + private IEnumerable? GetCompatibilityExamplesExtension() + { + if (Examples is null || Examples.Count == 0) + { + return null; + } + + if (Example is not null) + { + return Examples; + } + + return Examples.Count > 1 ? Examples.Skip(1) : null; + } + private static bool IsPowerOfTwo(int x) { return x != 0 && (x & (x - 1)) == 0; diff --git a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt index e763311ca..524d86c60 100644 --- a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt +++ b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt @@ -1,2 +1,3 @@ #nullable enable const Microsoft.OpenApi.OpenApiConstants.OaiLicenseIdentifier = "x-oai-license-identifier" -> string! +const Microsoft.OpenApi.OpenApiConstants.JsonSchemaExamplesExtension = "x-jsonschema-examples" -> string! diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs index af7f33d9c..25d36eff1 100644 --- a/src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs @@ -267,6 +267,10 @@ internal static partial class OpenApiV2Deserializer "example", (o, n, _, _) => o.Example = n }, + { + OpenApiConstants.JsonSchemaExamplesExtension, + (o, n, _, c) => o.Examples = n.CreateListOfAny(c) + }, { OpenApiConstants.PatternPropertiesExtension, (o, n, t, c) => o.PatternProperties = n.CreateMap(LoadSchema, t, c) diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs index 48dde9d5e..9b85fcf51 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs @@ -280,6 +280,10 @@ internal static partial class OpenApiV3Deserializer "example", (o, n, _, _) => o.Example = n }, + { + OpenApiConstants.JsonSchemaExamplesExtension, + (o, n, _, c) => o.Examples = n.CreateListOfAny(c) + }, { "deprecated", (o, n, _, _) => diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs index 14ec467aa..1b2a313d5 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs @@ -1746,6 +1746,82 @@ public async Task SerializePatternPropertiesAsExtensionInEarlierVersions(OpenApi Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual))); } + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + public async Task SerializeSingleExampleAsExampleInV2V3WhenExampleUnset(OpenApiSpecVersion version) + { + var expected = """ + { + "example": "example value" + } + """; + var schema = new OpenApiSchema + { + Examples = + [ + JsonValue.Create("example value")! + ] + }; + + var actual = await schema.SerializeAsJsonAsync(version); + + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual))); + } + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + public async Task SerializeMultipleExamplesInV2V3WhenExampleUnset(OpenApiSpecVersion version) + { + var expected = """ + { + "example": "example value", + "x-jsonschema-examples": [ + 42 + ] + } + """; + var schema = new OpenApiSchema + { + Examples = + [ + JsonValue.Create("example value")!, + JsonValue.Create(42)! + ] + }; + + var actual = await schema.SerializeAsJsonAsync(version); + + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual))); + } + + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + public async Task SerializeExamplesAsJsonSchemaKeywordInV31AndLater(OpenApiSpecVersion version) + { + var expected = """ + { + "examples": [ + "example value", + 42 + ] + } + """; + var schema = new OpenApiSchema + { + Examples = + [ + JsonValue.Create("example value")!, + JsonValue.Create(42)! + ] + }; + + var actual = await schema.SerializeAsJsonAsync(version); + + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual))); + } + [Theory] [InlineData(OpenApiSpecVersion.OpenApi2_0)] [InlineData(OpenApiSpecVersion.OpenApi3_0)] @@ -1837,6 +1913,76 @@ public void DeserializePatternPropertiesExtensionInV3AssignsPatternPropertiesPro Assert.True(schema.Extensions is null || !schema.Extensions.ContainsKey("x-jsonschema-patternProperties")); } + [Fact] + public void DeserializeExamplesExtensionInV2AssignsExamplesProperty() + { + var jsonContent = """ + { + "swagger": "2.0", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "definitions": { + "TestSchema": { + "type": "string", + "example": "primary example", + "x-jsonschema-examples": [ + "secondary example", + 42 + ] + } + } + } + """; + + var readResult = OpenApiDocument.Parse(jsonContent, "json"); + + Assert.Empty(readResult.Diagnostic.Errors); + var schema = readResult.Document.Components.Schemas["TestSchema"]; + Assert.Equal("primary example", schema.Example?.GetValue()); + Assert.NotNull(schema.Examples); + Assert.Collection( + schema.Examples, + example => Assert.Equal("secondary example", example.GetValue()), + example => Assert.Equal(42, example.GetValue())); + Assert.True(schema.Extensions is null || !schema.Extensions.ContainsKey(OpenApiConstants.JsonSchemaExamplesExtension)); + } + + [Fact] + public void DeserializeExamplesExtensionInV3AssignsExamplesProperty() + { + var jsonContent = """ + { + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "TestSchema": { + "type": "string", + "example": "primary example", + "x-jsonschema-examples": [ + "secondary example", + 42 + ] + } + } + } + } + """; + + var readResult = OpenApiDocument.Parse(jsonContent, "json"); + + Assert.Empty(readResult.Diagnostic.Errors); + var schema = readResult.Document.Components.Schemas["TestSchema"]; + Assert.Equal("primary example", schema.Example?.GetValue()); + Assert.NotNull(schema.Examples); + Assert.Collection( + schema.Examples, + example => Assert.Equal("secondary example", example.GetValue()), + example => Assert.Equal(42, example.GetValue())); + Assert.True(schema.Extensions is null || !schema.Extensions.ContainsKey(OpenApiConstants.JsonSchemaExamplesExtension)); + } + [Fact] public void DeserializeContainsExtensionsInV3AssignsContainsProperties() {