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
73 changes: 28 additions & 45 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@
/// <summary>
/// The Schema Object allows the definition of input and output data types.
/// </summary>
public class OpenApiSchema : IOpenApiAnnotatable, IOpenApiExtensible, IOpenApiReferenceable, IOpenApiSerializable
public class OpenApiSchema : IOpenApiAnnotatable, IOpenApiExtensible, IOpenApiReferenceable
{
private JsonNode _example;
private JsonNode _default;
private IList<JsonNode> _examples;

/// <summary>
/// Follow JSON Schema definition. Short text providing information about the data.
/// </summary>
Expand Down Expand Up @@ -148,11 +144,7 @@
/// 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.
/// </summary>
public virtual JsonNode Default
{
get => _default;
set => _default = value;
}
public virtual JsonNode Default { get; set; }

/// <summary>
/// Relevant only for Schema "properties" definitions. Declares the property as "read only".
Expand Down Expand Up @@ -273,22 +265,14 @@
/// 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.
/// </summary>
public virtual JsonNode Example
{
get => _example;
set => _example = value;
}
public virtual JsonNode Example { get; set; }

/// <summary>
/// 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.
/// </summary>
public virtual IList<JsonNode> Examples
{
get => _examples;
set => _examples = value;
}
public virtual IList<JsonNode> Examples { get; set; }

/// <summary>
/// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00
Expand Down Expand Up @@ -327,6 +311,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 @@ -373,7 +362,7 @@
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;

Check warning

Code scanning / CodeQL

Virtual call in constructor or destructor Warning

Avoid virtual calls in a constructor or destructor.
ReadOnly = schema?.ReadOnly ?? ReadOnly;
WriteOnly = schema?.WriteOnly ?? WriteOnly;
AllOf = schema?.AllOf != null ? new List<OpenApiSchema>(schema.AllOf) : null;
Expand All @@ -392,8 +381,8 @@
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<JsonNode>(schema.Examples) : null;
Example = schema?.Example != null ? JsonNodeCloneHelper.Clone(schema?.Example) : null;

Check warning

Code scanning / CodeQL

Virtual call in constructor or destructor Warning

Avoid virtual calls in a constructor or destructor.
Examples = schema?.Examples != null ? new List<JsonNode>(schema.Examples) : null;

Check warning

Code scanning / CodeQL

Virtual call in constructor or destructor Warning

Avoid virtual calls in a constructor or destructor.
Enum = schema?.Enum != null ? new List<JsonNode>(schema.Enum) : null;
Nullable = schema?.Nullable ?? Nullable;
ExternalDocs = schema?.ExternalDocs != null ? new(schema?.ExternalDocs) : null;
Expand All @@ -403,6 +392,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 +420,7 @@

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

// title
Expand Down Expand Up @@ -554,6 +544,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 +560,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 All @@ -577,7 +573,7 @@
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));
}

Expand Down Expand Up @@ -820,15 +816,9 @@
}
else
{
var list = new List<JsonSchemaType>();
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()));
}
}
Expand All @@ -850,16 +840,9 @@
{
// create a new array and insert the type and "null" as values
Type = type | JsonSchemaType.Null;
var list = new List<string>();
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));
}

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"
41 changes: 34 additions & 7 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down 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