diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 1adfc8c01..4b10a089d 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -43,7 +43,7 @@ public class OpenApiSchema : IOpenApiAnnotatable, IOpenApiExtensible, IOpenApiRe /// /// $vocabulary- used in meta-schemas to identify the vocabularies available for use in schemas described by that meta-schema. /// - public virtual string Vocabulary { get; set; } + public virtual IDictionary Vocabulary { get; set; } /// /// $dynamicRef - an applicator that allows for deferring the full resolution until runtime, at which point it is resolved each time it is encountered while evaluating an instance @@ -55,16 +55,6 @@ public class OpenApiSchema : IOpenApiAnnotatable, IOpenApiExtensible, IOpenApiRe /// public virtual string DynamicAnchor { get; set; } - /// - /// $recursiveAnchor - used to construct recursive schemas i.e one that has a reference to its own root, identified by the empty fragment URI reference ("#") - /// - public virtual string RecursiveAnchor { get; set; } - - /// - /// $recursiveRef - used to construct recursive schemas i.e one that has a reference to its own root, identified by the empty fragment URI reference ("#") - /// - public virtual string RecursiveRef { get; set; } - /// /// $defs - reserves a location for schema authors to inline re-usable JSON Schemas into a more general schema. /// The keyword does not directly affect the validation result @@ -358,11 +348,9 @@ public OpenApiSchema(OpenApiSchema schema) Id = schema?.Id ?? Id; Schema = schema?.Schema ?? Schema; Comment = schema?.Comment ?? Comment; - Vocabulary = schema?.Vocabulary ?? Vocabulary; + Vocabulary = schema?.Vocabulary != null ? new Dictionary(schema.Vocabulary) : null; DynamicAnchor = schema?.DynamicAnchor ?? DynamicAnchor; DynamicRef = schema?.DynamicRef ?? DynamicRef; - RecursiveAnchor = schema?.RecursiveAnchor ?? RecursiveAnchor; - RecursiveRef = schema?.RecursiveRef ?? RecursiveRef; Definitions = schema?.Definitions != null ? new Dictionary(schema.Definitions) : null; UnevaluatedProperties = schema?.UnevaluatedProperties ?? UnevaluatedProperties; V31ExclusiveMaximum = schema?.V31ExclusiveMaximum ?? V31ExclusiveMaximum; @@ -490,22 +478,22 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, SerializeTypeProperty(Type, writer, version); // allOf - writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, (w, s) => s.SerializeAsV3(w)); + writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, callback); // anyOf - writer.WriteOptionalCollection(OpenApiConstants.AnyOf, AnyOf, (w, s) => s.SerializeAsV3(w)); + writer.WriteOptionalCollection(OpenApiConstants.AnyOf, AnyOf, callback); // oneOf - writer.WriteOptionalCollection(OpenApiConstants.OneOf, OneOf, (w, s) => s.SerializeAsV3(w)); + writer.WriteOptionalCollection(OpenApiConstants.OneOf, OneOf, callback); // not - writer.WriteOptionalObject(OpenApiConstants.Not, Not, (w, s) => s.SerializeAsV3(w)); + writer.WriteOptionalObject(OpenApiConstants.Not, Not, callback); // items - writer.WriteOptionalObject(OpenApiConstants.Items, Items, (w, s) => s.SerializeAsV3(w)); + writer.WriteOptionalObject(OpenApiConstants.Items, Items, callback); // properties - writer.WriteOptionalMap(OpenApiConstants.Properties, Properties, (w, s) => s.SerializeAsV3(w)); + writer.WriteOptionalMap(OpenApiConstants.Properties, Properties, callback); // additionalProperties if (AdditionalPropertiesAllowed) @@ -513,7 +501,7 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, writer.WriteOptionalObject( OpenApiConstants.AdditionalProperties, AdditionalProperties, - (w, s) => s.SerializeAsV3(w)); + callback); } else { @@ -536,7 +524,7 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, } // discriminator - writer.WriteOptionalObject(OpenApiConstants.Discriminator, Discriminator, (w, s) => s.SerializeAsV3(w)); + writer.WriteOptionalObject(OpenApiConstants.Discriminator, Discriminator, callback); // readOnly writer.WriteProperty(OpenApiConstants.ReadOnly, ReadOnly, false); @@ -548,7 +536,7 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, writer.WriteOptionalObject(OpenApiConstants.Xml, Xml, (w, s) => s.SerializeAsV2(w)); // externalDocs - writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, (w, s) => s.SerializeAsV3(w)); + writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, callback); // example writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e)); @@ -557,7 +545,7 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated, false); // extensions - writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); + writer.WriteExtensions(Extensions, version); writer.WriteEndObject(); } @@ -574,12 +562,10 @@ internal void WriteV31Properties(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.Id, Id); writer.WriteProperty(OpenApiConstants.DollarSchema, Schema); writer.WriteProperty(OpenApiConstants.Comment, Comment); - writer.WriteProperty(OpenApiConstants.Vocabulary, Vocabulary); - writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, (w, s) => s.SerializeAsV3(w)); + writer.WriteOptionalMap(OpenApiConstants.Vocabulary, Vocabulary, (w, s) => w.WriteValue(s)); + writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, (w, s) => s.SerializeAsV31(w)); writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef); writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor); - writer.WriteProperty(OpenApiConstants.RecursiveAnchor, RecursiveAnchor); - writer.WriteProperty(OpenApiConstants.RecursiveRef, RecursiveRef); writer.WriteProperty(OpenApiConstants.V31ExclusiveMaximum, V31ExclusiveMaximum); writer.WriteProperty(OpenApiConstants.V31ExclusiveMinimum, V31ExclusiveMinimum); writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties, false); diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs index 535a6a522..759959344 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs @@ -74,16 +74,12 @@ internal OpenApiSchemaReference(OpenApiSchema target, string referenceId) /// public override string Comment { get => Target.Comment; set => Target.Comment = value; } /// - public override string Vocabulary { get => Target.Vocabulary; set => Target.Vocabulary = value; } + public override IDictionary Vocabulary { get => Target.Vocabulary; set => Target.Vocabulary = value; } /// public override string DynamicRef { get => Target.DynamicRef; set => Target.DynamicRef = value; } /// public override string DynamicAnchor { get => Target.DynamicAnchor; set => Target.DynamicAnchor = value; } /// - public override string RecursiveAnchor { get => Target.RecursiveAnchor; set => Target.RecursiveAnchor = value; } - /// - public override string RecursiveRef { get => Target.RecursiveRef; set => Target.RecursiveRef = value; } - /// public override IDictionary Definitions { get => Target.Definitions; set => Target.Definitions = value; } /// public override decimal? V31ExclusiveMaximum { get => Target.V31ExclusiveMaximum; set => Target.V31ExclusiveMaximum = value; } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs index 7757c710f..05ce240c9 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs @@ -32,7 +32,7 @@ internal static partial class OpenApiV31Deserializer }, { "$vocabulary", - (o, n, _) => o.Vocabulary = n.GetScalarValue() + (o, n, _) => o.Vocabulary = n.CreateSimpleMap(LoadBool) }, { "$dynamicRef", @@ -42,14 +42,6 @@ internal static partial class OpenApiV31Deserializer "$dynamicAnchor", (o, n, _) => o.DynamicAnchor = n.GetScalarValue() }, - { - "$recursiveAnchor", - (o, n, _) => o.RecursiveAnchor = n.GetScalarValue() - }, - { - "$recursiveRef", - (o, n, _) => o.RecursiveRef = n.GetScalarValue() - }, { "$defs", (o, n, t) => o.Definitions = n.CreateMap(LoadSchema, t) diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs index d6c9d0fcf..a037dc3c1 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs @@ -145,6 +145,11 @@ private static string LoadString(ParseNode node) return node.GetScalarValue(); } + private static bool LoadBool(ParseNode node) + { + return bool.Parse(node.GetScalarValue()); + } + private static (string, string) GetReferenceIdAndExternalResource(string pointer) { /* Check whether the reference pointer is a URL diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs index f6102f316..a1a74e815 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs @@ -272,6 +272,25 @@ public static void WriteOptionalMap( } } + /// + /// Write the optional Open API element map (string to string mapping). + /// + /// The Open API writer. + /// The property name. + /// The map values. + /// The map element writer action. + public static void WriteOptionalMap( + this IOpenApiWriter writer, + string name, + IDictionary elements, + Action action) + { + if (elements != null && elements.Any()) + { + writer.WriteMapInternal(name, elements, action); + } + } + /// /// Write the optional Open API element map. /// diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs index cacb1ed86..6df59331e 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System.Collections.Generic; @@ -404,5 +404,51 @@ public void LoadSchemaWithNullableExtensionAsV31Works(string filePath) // Assert schema.Type.Should().BeEquivalentTo(new string[] { "string", "null" }); } + + [Fact] + public void SerializeSchemaWithJsonSchemaKeywordsWorks() + { + // Arrange + var expected = @"$id: https://example.com/schemas/person.schema.yaml +$schema: https://json-schema.org/draft/2020-12/schema +$comment: A schema defining a person object with optional references to dynamic components. +$vocabulary: + https://json-schema.org/draft/2020-12/vocab/core: true + https://json-schema.org/draft/2020-12/vocab/applicator: true + https://json-schema.org/draft/2020-12/vocab/validation: true + https://json-schema.org/draft/2020-12/vocab/meta-data: false + https://json-schema.org/draft/2020-12/vocab/format-annotation: false +$dynamicAnchor: addressDef +title: Person +required: + - name +type: object +properties: + name: + $comment: The person's full name + type: string + age: + $comment: Age must be a non-negative integer + minimum: 0 + type: integer + address: + $comment: Reference to an address definition which can change dynamically + $dynamicRef: '#addressDef' +description: Schema for a person object +"; + var path = Path.Combine(SampleFolderPath, "schemaWithJsonSchemaKeywords.yaml"); + + // Act + var schema = OpenApiModelFactory.Load(path, OpenApiSpecVersion.OpenApi3_1, out _); + + // serialization + var writer = new StringWriter(); + schema.SerializeAsV31(new OpenApiYamlWriter(writer)); + var schemaString = writer.ToString(); + + // Assert + schema.Vocabulary.Keys.Count.Should().Be(5); + schemaString.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral()); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/schemaWithJsonSchemaKeywords.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/schemaWithJsonSchemaKeywords.yaml new file mode 100644 index 000000000..3d88cffcd --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/schemaWithJsonSchemaKeywords.yaml @@ -0,0 +1,41 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "https://example.com/schemas/person.schema.yaml" +$comment: "A schema defining a person object with optional references to dynamic components." +$vocabulary: + "https://json-schema.org/draft/2020-12/vocab/core": true + "https://json-schema.org/draft/2020-12/vocab/applicator": true + "https://json-schema.org/draft/2020-12/vocab/validation": true + "https://json-schema.org/draft/2020-12/vocab/meta-data": false + "https://json-schema.org/draft/2020-12/vocab/format-annotation": false + +title: "Person" +description: "Schema for a person object" +type: "object" + +properties: + name: + type: "string" + $comment: "The person's full name" + age: + type: "integer" + minimum: 0 + $comment: "Age must be a non-negative integer" + address: + $dynamicRef: "#addressDef" + $comment: "Reference to an address definition which can change dynamically" + +required: + - name + +$dynamicAnchor: "addressDef" +definitions: + address: + $dynamicAnchor: "addressDef" + type: "object" + properties: + street: + type: "string" + city: + type: "string" + postalCode: + type: "string" diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 3a7fdbd57..dabe527c7 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -901,8 +901,6 @@ namespace Microsoft.OpenApi.Models public virtual System.Collections.Generic.IDictionary PatternProperties { get; set; } public virtual System.Collections.Generic.IDictionary Properties { get; set; } public virtual bool ReadOnly { get; set; } - public virtual string RecursiveAnchor { get; set; } - public virtual string RecursiveRef { get; set; } public virtual Microsoft.OpenApi.Models.OpenApiReference Reference { get; set; } public virtual System.Collections.Generic.ISet Required { get; set; } public virtual string Schema { get; set; } @@ -914,7 +912,7 @@ namespace Microsoft.OpenApi.Models public virtual bool UnresolvedReference { get; set; } public virtual decimal? V31ExclusiveMaximum { get; set; } public virtual decimal? V31ExclusiveMinimum { get; set; } - public virtual string Vocabulary { get; set; } + public virtual System.Collections.Generic.IDictionary Vocabulary { get; set; } public virtual bool WriteOnly { get; set; } public virtual Microsoft.OpenApi.Models.OpenApiXml Xml { get; set; } public virtual void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } @@ -1243,8 +1241,6 @@ namespace Microsoft.OpenApi.Models.References public override System.Collections.Generic.IDictionary PatternProperties { get; set; } public override System.Collections.Generic.IDictionary Properties { get; set; } public override bool ReadOnly { get; set; } - public override string RecursiveAnchor { get; set; } - public override string RecursiveRef { get; set; } public override System.Collections.Generic.ISet Required { get; set; } public override string Schema { get; set; } public override string Title { get; set; } @@ -1254,7 +1250,7 @@ namespace Microsoft.OpenApi.Models.References public override bool? UniqueItems { get; set; } public override decimal? V31ExclusiveMaximum { get; set; } public override decimal? V31ExclusiveMinimum { get; set; } - public override string Vocabulary { get; set; } + public override System.Collections.Generic.IDictionary Vocabulary { get; set; } public override bool WriteOnly { get; set; } public override Microsoft.OpenApi.Models.OpenApiXml Xml { get; set; } public override void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } @@ -1849,6 +1845,7 @@ namespace Microsoft.OpenApi.Writers { public static void WriteOptionalCollection(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IEnumerable elements, System.Action action) { } public static void WriteOptionalCollection(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IEnumerable elements, System.Action action) { } + public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary elements, System.Action action) { } public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary elements, System.Action action) { } public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary elements, System.Action action) where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { }