diff --git a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs
index 8877faac8..1c016f4c4 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs
@@ -480,6 +480,11 @@ public static class OpenApiConstants
///
public const string Properties = "properties";
+ ///
+ /// Field: UnrecognizedKeywords
+ ///
+ public const string UnrecognizedKeywords = "unrecognizedKeywords";
+
///
/// Field: Pattern Properties
///
diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
index 0215ac522..c9e5441a9 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
@@ -15,12 +15,8 @@ namespace Microsoft.OpenApi.Models
///
/// The Schema Object allows the definition of input and output data types.
///
- public class OpenApiSchema : IOpenApiAnnotatable, IOpenApiExtensible, IOpenApiReferenceable, IOpenApiSerializable
+ public class OpenApiSchema : IOpenApiAnnotatable, IOpenApiExtensible, IOpenApiReferenceable
{
- private JsonNode _example;
- private JsonNode _default;
- private IList _examples;
-
///
/// Follow JSON Schema definition. Short text providing information about the data.
///
@@ -148,11 +144,7 @@ public class OpenApiSchema : IOpenApiAnnotatable, IOpenApiExtensible, IOpenApiRe
/// Unlike JSON Schema, the value MUST conform to the defined type for the Schema Object defined at the same level.
/// For example, if type is string, then default can be "foo" but cannot be 1.
///
- public virtual JsonNode Default
- {
- get => _default;
- set => _default = value;
- }
+ public virtual JsonNode Default { get; set; }
///
/// Relevant only for Schema "properties" definitions. Declares the property as "read only".
@@ -273,22 +265,14 @@ public virtual JsonNode Default
/// To represent examples that cannot be naturally represented in JSON or YAML,
/// a string value can be used to contain the example with escaping where necessary.
///
- public virtual JsonNode Example
- {
- get => _example;
- set => _example = value;
- }
+ public virtual JsonNode Example { get; set; }
///
/// A free-form property to include examples of an instance for this schema.
/// To represent examples that cannot be naturally represented in JSON or YAML,
/// a list of values can be used to contain the examples with escaping where necessary.
///
- public virtual IList Examples
- {
- get => _examples;
- set => _examples = value;
- }
+ public virtual IList Examples { get; set; }
///
/// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00
@@ -327,6 +311,11 @@ public virtual IList Examples
///
public virtual IDictionary Extensions { get; set; } = new Dictionary();
+ ///
+ /// This object stores any unrecognized keywords found in the schema.
+ ///
+ public virtual IDictionary UnrecognizedKeywords { get; set; } = new Dictionary();
+
///
/// Indicates object is a placeholder reference to an actual object and does not contain valid data.
///
@@ -373,7 +362,7 @@ public OpenApiSchema(OpenApiSchema schema)
MinLength = schema?.MinLength ?? MinLength;
Pattern = schema?.Pattern ?? Pattern;
MultipleOf = schema?.MultipleOf ?? MultipleOf;
- _default = schema?.Default != null ? JsonNodeCloneHelper.Clone(schema?.Default) : null;
+ Default = schema?.Default != null ? JsonNodeCloneHelper.Clone(schema?.Default) : null;
ReadOnly = schema?.ReadOnly ?? ReadOnly;
WriteOnly = schema?.WriteOnly ?? WriteOnly;
AllOf = schema?.AllOf != null ? new List(schema.AllOf) : null;
@@ -392,8 +381,8 @@ public OpenApiSchema(OpenApiSchema schema)
AdditionalPropertiesAllowed = schema?.AdditionalPropertiesAllowed ?? AdditionalPropertiesAllowed;
AdditionalProperties = schema?.AdditionalProperties != null ? new(schema?.AdditionalProperties) : null;
Discriminator = schema?.Discriminator != null ? new(schema?.Discriminator) : null;
- _example = schema?.Example != null ? JsonNodeCloneHelper.Clone(schema?.Example) : null;
- _examples = schema?.Examples != null ? new List(schema.Examples) : null;
+ Example = schema?.Example != null ? JsonNodeCloneHelper.Clone(schema?.Example) : null;
+ Examples = schema?.Examples != null ? new List(schema.Examples) : null;
Enum = schema?.Enum != null ? new List(schema.Enum) : null;
Nullable = schema?.Nullable ?? Nullable;
ExternalDocs = schema?.ExternalDocs != null ? new(schema?.ExternalDocs) : null;
@@ -403,6 +392,7 @@ public OpenApiSchema(OpenApiSchema schema)
UnresolvedReference = schema?.UnresolvedReference ?? UnresolvedReference;
Reference = schema?.Reference != null ? new(schema?.Reference) : null;
Annotations = schema?.Annotations != null ? new Dictionary(schema?.Annotations) : null;
+ UnrecognizedKeywords = schema?.UnrecognizedKeywords != null ? new Dictionary(schema?.UnrecognizedKeywords) : null;
}
///
@@ -430,7 +420,7 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
if (version == OpenApiSpecVersion.OpenApi3_1)
{
- WriteV31Properties(writer);
+ WriteJsonSchemaKeywords(writer);
}
// title
@@ -554,6 +544,12 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
// extensions
writer.WriteExtensions(Extensions, version);
+ // Unrecognized keywords
+ if (UnrecognizedKeywords.Any())
+ {
+ writer.WriteOptionalMap(OpenApiConstants.UnrecognizedKeywords, UnrecognizedKeywords, (w,s) => w.WriteAny(s));
+ }
+
writer.WriteEndObject();
}
@@ -564,7 +560,7 @@ public virtual void SerializeAsV2(IOpenApiWriter writer)
SerializeAsV2(writer: writer, parentRequiredProperties: new HashSet(), propertyName: null);
}
- internal void WriteV31Properties(IOpenApiWriter writer)
+ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer)
{
writer.WriteProperty(OpenApiConstants.Id, Id);
writer.WriteProperty(OpenApiConstants.DollarSchema, Schema);
@@ -577,7 +573,7 @@ internal void WriteV31Properties(IOpenApiWriter writer)
writer.WriteProperty(OpenApiConstants.V31ExclusiveMaximum, V31ExclusiveMaximum);
writer.WriteProperty(OpenApiConstants.V31ExclusiveMinimum, V31ExclusiveMinimum);
writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties, false);
- writer.WriteOptionalCollection(OpenApiConstants.Examples, _examples, (nodeWriter, s) => nodeWriter.WriteAny(s));
+ writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (nodeWriter, s) => nodeWriter.WriteAny(s));
writer.WriteOptionalMap(OpenApiConstants.PatternProperties, PatternProperties, (w, s) => s.SerializeAsV31(w));
}
@@ -820,15 +816,9 @@ private void SerializeTypeProperty(JsonSchemaType? type, IOpenApiWriter writer,
}
else
{
- var list = new List();
- foreach (JsonSchemaType flag in jsonSchemaTypeValues)
- {
- if (type.Value.HasFlag(flag))
- {
- list.Add(flag);
- }
- }
-
+ var list = (from JsonSchemaType flag in jsonSchemaTypeValues
+ where type.Value.HasFlag(flag)
+ select flag).ToList();
writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) => w.WriteValue(s.ToIdentifier()));
}
}
@@ -850,16 +840,9 @@ private void UpCastSchemaTypeToV31(JsonSchemaType? type, IOpenApiWriter writer)
{
// create a new array and insert the type and "null" as values
Type = type | JsonSchemaType.Null;
- var list = new List();
- foreach (JsonSchemaType? flag in jsonSchemaTypeValues)
- {
- // Check if the flag is set in 'type' using a bitwise AND operation
- if (Type.Value.HasFlag(flag))
- {
- list.Add(flag.ToIdentifier());
- }
- }
-
+ var list = (from JsonSchemaType? flag in jsonSchemaTypeValues// Check if the flag is set in 'type' using a bitwise AND operation
+ where Type.Value.HasFlag(flag)
+ select flag.ToIdentifier()).ToList();
writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) => w.WriteValue(s));
}
diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs
index 9c035da0d..ad943dce4 100644
--- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs
+++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs
@@ -7,6 +7,8 @@
using Microsoft.OpenApi.Reader.ParseNodes;
using System.Collections.Generic;
using System.Globalization;
+using System.Linq;
+using System.Text.Json.Nodes;
namespace Microsoft.OpenApi.Reader.V31
{
@@ -254,7 +256,17 @@ public static OpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocum
foreach (var propertyNode in mapNode)
{
- propertyNode.ParseField(schema, _openApiSchemaFixedFields, _openApiSchemaPatternFields);
+ bool isRecognized = _openApiSchemaFixedFields.ContainsKey(propertyNode.Name) ||
+ _openApiSchemaPatternFields.Any(p => p.Key(propertyNode.Name));
+
+ if (isRecognized)
+ {
+ propertyNode.ParseField(schema, _openApiSchemaFixedFields, _openApiSchemaPatternFields);
+ }
+ else
+ {
+ schema.UnrecognizedKeywords[propertyNode.Name] = propertyNode.JsonNode;
+ }
}
if (schema.Extensions.ContainsKey(OpenApiConstants.NullableExtension))
diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs
index a1a74e815..8c49a2960 100644
--- a/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs
+++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs
@@ -5,6 +5,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
+using System.Text.Json.Nodes;
using Microsoft.OpenApi.Interfaces;
namespace Microsoft.OpenApi.Writers
@@ -253,6 +254,25 @@ public static void WriteRequiredMap(
writer.WriteMapInternal(name, elements, action);
}
+ ///
+ /// 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 (string to string mapping).
///
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs
index 5f149b021..4a6fdac50 100644
--- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs
+++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs
@@ -495,5 +495,21 @@ public void ParseSchemaWithConstWorks()
var schemaString = writer.ToString();
schemaString.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
}
+
+ [Fact]
+ public void ParseSchemaWithUnrecognizedKeywordsWorks()
+ {
+ var input = @"{
+ ""type"": ""string"",
+ ""format"": ""date-time"",
+ ""customKeyword"": ""customValue"",
+ ""anotherKeyword"": 42,
+ ""x-test"": ""test""
+}
+";
+ var schema = OpenApiModelFactory.Parse(input, OpenApiSpecVersion.OpenApi3_1, out _, "json");
+ schema.UnrecognizedKeywords.Should().HaveCount(2);
+ }
+
}
}
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/schemaWithJsonSchemaKeywords.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/schemaWithJsonSchemaKeywords.yaml
index 3d88cffcd..8c4fb1c1b 100644
--- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/schemaWithJsonSchemaKeywords.yaml
+++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/schemaWithJsonSchemaKeywords.yaml
@@ -28,14 +28,3 @@ 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/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
index 408173e6e..75ea5ca47 100644
--- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
+++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
@@ -22,7 +22,7 @@ namespace Microsoft.OpenApi.Tests.Models
[Collection("DefaultSettings")]
public class OpenApiSchemaTests
{
- public static OpenApiSchema BasicSchema = new();
+ private static readonly OpenApiSchema BasicSchema = new();
public static readonly OpenApiSchema AdvancedSchemaNumber = new()
{
@@ -602,15 +602,42 @@ public void OpenApiWalkerVisitsOpenApiSchemaNot()
// Assert
visitor.Titles.Count.Should().Be(2);
}
- }
- internal class SchemaVisitor : OpenApiVisitorBase
- {
- public List Titles = new();
+ [Fact]
+ public void SerializeSchemaWithUnrecognizedPropertiesWorks()
+ {
+ // Arrange
+ var schema = new OpenApiSchema
+ {
+ UnrecognizedKeywords = new Dictionary()
+ {
+ ["customKeyWord"] = "bar",
+ ["anotherKeyword"] = 42
+ }
+ };
- public override void Visit(OpenApiSchema schema)
+ var expected = @"{
+ ""unrecognizedKeywords"": {
+ ""customKeyWord"": ""bar"",
+ ""anotherKeyword"": 42
+ }
+}";
+
+ // Act
+ var actual = schema.SerializeAsJson(OpenApiSpecVersion.OpenApi3_1);
+
+ // Assert
+ actual.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
+ }
+
+ internal class SchemaVisitor : OpenApiVisitorBase
{
- Titles.Add(schema.Title);
+ public List Titles = new();
+
+ public override void Visit(OpenApiSchema schema)
+ {
+ Titles.Add(schema.Title);
+ }
}
}
}
diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
index 8f9f8ed41..98fdd33a5 100644
--- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
+++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
@@ -512,6 +512,7 @@ namespace Microsoft.OpenApi.Models
public const string Type = "type";
public const string UnevaluatedProperties = "unevaluatedProperties";
public const string UniqueItems = "uniqueItems";
+ public const string UnrecognizedKeywords = "unrecognizedKeywords";
public const string Url = "url";
public const string V2ReferenceUri = "https://registry/definitions/";
public const string V31ExclusiveMaximum = "exclusiveMaximum";
@@ -925,6 +926,7 @@ namespace Microsoft.OpenApi.Models
public virtual bool UnEvaluatedProperties { get; set; }
public virtual bool UnevaluatedProperties { get; set; }
public virtual bool? UniqueItems { get; set; }
+ public virtual System.Collections.Generic.IDictionary UnrecognizedKeywords { get; set; }
public virtual bool UnresolvedReference { get; set; }
public virtual decimal? V31ExclusiveMaximum { get; set; }
public virtual decimal? V31ExclusiveMinimum { get; set; }
@@ -1859,6 +1861,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 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) { }
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 { }
public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary elements, System.Action action)