Skip to content
Merged
5 changes: 5 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,11 @@ public static class OpenApiConstants
/// </summary>
public const string Properties = "properties";

/// <summary>
/// Field: UnrecognizedKeywords
/// </summary>
public const string UnrecognizedKeywords = "unrecognizedKeywords";

/// <summary>
/// Field: Pattern Properties
/// </summary>
Expand Down
16 changes: 14 additions & 2 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,11 @@
/// </summary>
public virtual IDictionary<string, IOpenApiExtension> Extensions { get; set; } = new Dictionary<string, IOpenApiExtension>();

/// <summary>
/// This object stores any unrecognized keywords found in the schema.
/// </summary>
public virtual IDictionary<string, JsonNode> UnrecognizedKeywords { get; set; } = new Dictionary<string, JsonNode>();

/// <summary>
/// Indicates object is a placeholder reference to an actual object and does not contain valid data.
/// </summary>
Expand Down Expand Up @@ -403,6 +408,7 @@
UnresolvedReference = schema?.UnresolvedReference ?? UnresolvedReference;
Reference = schema?.Reference != null ? new(schema?.Reference) : null;
Annotations = schema?.Annotations != null ? new Dictionary<string, object>(schema?.Annotations) : null;
UnrecognizedKeywords = schema?.UnrecognizedKeywords != null ? new Dictionary<string, JsonNode>(schema?.UnrecognizedKeywords) : null;

Check warning

Code scanning / CodeQL

Virtual call in constructor or destructor Warning

Avoid virtual calls in a constructor or destructor.
}

/// <summary>
Expand Down Expand Up @@ -430,7 +436,7 @@

if (version == OpenApiSpecVersion.OpenApi3_1)
{
WriteV31Properties(writer);
WriteJsonSchemaKeywords(writer);
}

// title
Expand Down Expand Up @@ -554,6 +560,12 @@
// extensions
writer.WriteExtensions(Extensions, version);

// Unrecognized keywords
if (UnrecognizedKeywords.Any())
{
writer.WriteOptionalMap(OpenApiConstants.UnrecognizedKeywords, UnrecognizedKeywords, (w,s) => w.WriteAny(s));
}

writer.WriteEndObject();
}

Expand All @@ -564,7 +576,7 @@
SerializeAsV2(writer: writer, parentRequiredProperties: new HashSet<string>(), propertyName: null);
}

internal void WriteV31Properties(IOpenApiWriter writer)
internal void WriteJsonSchemaKeywords(IOpenApiWriter writer)
{
writer.WriteProperty(OpenApiConstants.Id, Id);
writer.WriteProperty(OpenApiConstants.DollarSchema, Schema);
Expand Down
14 changes: 13 additions & 1 deletion src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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))
Expand Down
20 changes: 20 additions & 0 deletions src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -253,6 +254,25 @@ public static void WriteRequiredMap(
writer.WriteMapInternal(name, elements, action);
}

/// <summary>
/// Write the optional Open API element map (string to string mapping).
/// </summary>
/// <param name="writer">The Open API writer.</param>
/// <param name="name">The property name.</param>
/// <param name="elements">The map values.</param>
/// <param name="action">The map element writer action.</param>
public static void WriteOptionalMap(
this IOpenApiWriter writer,
string name,
IDictionary<string, JsonNode> elements,
Action<IOpenApiWriter, JsonNode> action)
{
if (elements != null && elements.Any())
{
writer.WriteMapInternal(name, elements, action);
}
}

/// <summary>
/// Write the optional Open API element map (string to string mapping).
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<OpenApiSchema>(input, OpenApiSpecVersion.OpenApi3_1, out _, "json");
schema.UnrecognizedKeywords.Should().HaveCount(2);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,3 @@ required:
- name

$dynamicAnchor: "addressDef"
definitions:
address:
$dynamicAnchor: "addressDef"
type: "object"
properties:
street:
type: "string"
city:
type: "string"
postalCode:
type: "string"
39 changes: 33 additions & 6 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -602,15 +602,42 @@ public void OpenApiWalkerVisitsOpenApiSchemaNot()
// Assert
visitor.Titles.Count.Should().Be(2);
}
}

internal class SchemaVisitor : OpenApiVisitorBase
{
public List<string> Titles = new();
[Fact]
public void SerializeSchemaWithUnrecognizedPropertiesWorks()
{
// Arrange
var schema = new OpenApiSchema
{
UnrecognizedKeywords = new Dictionary<string, JsonNode>()
{
["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<string> Titles = new();

public override void Visit(OpenApiSchema schema)
{
Titles.Add(schema.Title);
}
}
}
}
3 changes: 3 additions & 0 deletions test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<string, System.Text.Json.Nodes.JsonNode> UnrecognizedKeywords { get; set; }
public virtual bool UnresolvedReference { get; set; }
public virtual decimal? V31ExclusiveMaximum { get; set; }
public virtual decimal? V31ExclusiveMinimum { get; set; }
Expand Down Expand Up @@ -1859,6 +1861,7 @@ namespace Microsoft.OpenApi.Writers
public static void WriteOptionalCollection<T>(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IEnumerable<T> elements, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, T> action) { }
public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary<string, bool> elements, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, bool> action) { }
public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary<string, string> elements, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, string> action) { }
public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary<string, System.Text.Json.Nodes.JsonNode> elements, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, System.Text.Json.Nodes.JsonNode> action) { }
public static void WriteOptionalMap<T>(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary<string, T> elements, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, T> action)
where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { }
public static void WriteOptionalMap<T>(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary<string, T> elements, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, string, T> action)
Expand Down
Loading