Skip to content

Commit 8bf3fbe

Browse files
authored
Merge pull request #1982 from microsoft/mk/support-unrecognized-keywords
Add support for handling unrecognized keywords
2 parents 6899424 + 2443fa0 commit 8bf3fbe

File tree

8 files changed

+119
-64
lines changed

8 files changed

+119
-64
lines changed

src/Microsoft.OpenApi/Models/OpenApiConstants.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,11 @@ public static class OpenApiConstants
480480
/// </summary>
481481
public const string Properties = "properties";
482482

483+
/// <summary>
484+
/// Field: UnrecognizedKeywords
485+
/// </summary>
486+
public const string UnrecognizedKeywords = "unrecognizedKeywords";
487+
483488
/// <summary>
484489
/// Field: Pattern Properties
485490
/// </summary>

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

Lines changed: 28 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,8 @@ namespace Microsoft.OpenApi.Models
1515
/// <summary>
1616
/// The Schema Object allows the definition of input and output data types.
1717
/// </summary>
18-
public class OpenApiSchema : IOpenApiAnnotatable, IOpenApiExtensible, IOpenApiReferenceable, IOpenApiSerializable
18+
public class OpenApiSchema : IOpenApiAnnotatable, IOpenApiExtensible, IOpenApiReferenceable
1919
{
20-
private JsonNode _example;
21-
private JsonNode _default;
22-
private IList<JsonNode> _examples;
23-
2420
/// <summary>
2521
/// Follow JSON Schema definition. Short text providing information about the data.
2622
/// </summary>
@@ -148,11 +144,7 @@ public class OpenApiSchema : IOpenApiAnnotatable, IOpenApiExtensible, IOpenApiRe
148144
/// Unlike JSON Schema, the value MUST conform to the defined type for the Schema Object defined at the same level.
149145
/// For example, if type is string, then default can be "foo" but cannot be 1.
150146
/// </summary>
151-
public virtual JsonNode Default
152-
{
153-
get => _default;
154-
set => _default = value;
155-
}
147+
public virtual JsonNode Default { get; set; }
156148

157149
/// <summary>
158150
/// Relevant only for Schema "properties" definitions. Declares the property as "read only".
@@ -273,22 +265,14 @@ public virtual JsonNode Default
273265
/// To represent examples that cannot be naturally represented in JSON or YAML,
274266
/// a string value can be used to contain the example with escaping where necessary.
275267
/// </summary>
276-
public virtual JsonNode Example
277-
{
278-
get => _example;
279-
set => _example = value;
280-
}
268+
public virtual JsonNode Example { get; set; }
281269

282270
/// <summary>
283271
/// A free-form property to include examples of an instance for this schema.
284272
/// To represent examples that cannot be naturally represented in JSON or YAML,
285273
/// a list of values can be used to contain the examples with escaping where necessary.
286274
/// </summary>
287-
public virtual IList<JsonNode> Examples
288-
{
289-
get => _examples;
290-
set => _examples = value;
291-
}
275+
public virtual IList<JsonNode> Examples { get; set; }
292276

293277
/// <summary>
294278
/// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00
@@ -327,6 +311,11 @@ public virtual IList<JsonNode> Examples
327311
/// </summary>
328312
public virtual IDictionary<string, IOpenApiExtension> Extensions { get; set; } = new Dictionary<string, IOpenApiExtension>();
329313

314+
/// <summary>
315+
/// This object stores any unrecognized keywords found in the schema.
316+
/// </summary>
317+
public virtual IDictionary<string, JsonNode> UnrecognizedKeywords { get; set; } = new Dictionary<string, JsonNode>();
318+
330319
/// <summary>
331320
/// Indicates object is a placeholder reference to an actual object and does not contain valid data.
332321
/// </summary>
@@ -373,7 +362,7 @@ public OpenApiSchema(OpenApiSchema schema)
373362
MinLength = schema?.MinLength ?? MinLength;
374363
Pattern = schema?.Pattern ?? Pattern;
375364
MultipleOf = schema?.MultipleOf ?? MultipleOf;
376-
_default = schema?.Default != null ? JsonNodeCloneHelper.Clone(schema?.Default) : null;
365+
Default = schema?.Default != null ? JsonNodeCloneHelper.Clone(schema?.Default) : null;
377366
ReadOnly = schema?.ReadOnly ?? ReadOnly;
378367
WriteOnly = schema?.WriteOnly ?? WriteOnly;
379368
AllOf = schema?.AllOf != null ? new List<OpenApiSchema>(schema.AllOf) : null;
@@ -392,8 +381,8 @@ public OpenApiSchema(OpenApiSchema schema)
392381
AdditionalPropertiesAllowed = schema?.AdditionalPropertiesAllowed ?? AdditionalPropertiesAllowed;
393382
AdditionalProperties = schema?.AdditionalProperties != null ? new(schema?.AdditionalProperties) : null;
394383
Discriminator = schema?.Discriminator != null ? new(schema?.Discriminator) : null;
395-
_example = schema?.Example != null ? JsonNodeCloneHelper.Clone(schema?.Example) : null;
396-
_examples = schema?.Examples != null ? new List<JsonNode>(schema.Examples) : null;
384+
Example = schema?.Example != null ? JsonNodeCloneHelper.Clone(schema?.Example) : null;
385+
Examples = schema?.Examples != null ? new List<JsonNode>(schema.Examples) : null;
397386
Enum = schema?.Enum != null ? new List<JsonNode>(schema.Enum) : null;
398387
Nullable = schema?.Nullable ?? Nullable;
399388
ExternalDocs = schema?.ExternalDocs != null ? new(schema?.ExternalDocs) : null;
@@ -403,6 +392,7 @@ public OpenApiSchema(OpenApiSchema schema)
403392
UnresolvedReference = schema?.UnresolvedReference ?? UnresolvedReference;
404393
Reference = schema?.Reference != null ? new(schema?.Reference) : null;
405394
Annotations = schema?.Annotations != null ? new Dictionary<string, object>(schema?.Annotations) : null;
395+
UnrecognizedKeywords = schema?.UnrecognizedKeywords != null ? new Dictionary<string, JsonNode>(schema?.UnrecognizedKeywords) : null;
406396
}
407397

408398
/// <summary>
@@ -430,7 +420,7 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
430420

431421
if (version == OpenApiSpecVersion.OpenApi3_1)
432422
{
433-
WriteV31Properties(writer);
423+
WriteJsonSchemaKeywords(writer);
434424
}
435425

436426
// title
@@ -554,6 +544,12 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
554544
// extensions
555545
writer.WriteExtensions(Extensions, version);
556546

547+
// Unrecognized keywords
548+
if (UnrecognizedKeywords.Any())
549+
{
550+
writer.WriteOptionalMap(OpenApiConstants.UnrecognizedKeywords, UnrecognizedKeywords, (w,s) => w.WriteAny(s));
551+
}
552+
557553
writer.WriteEndObject();
558554
}
559555

@@ -564,7 +560,7 @@ public virtual void SerializeAsV2(IOpenApiWriter writer)
564560
SerializeAsV2(writer: writer, parentRequiredProperties: new HashSet<string>(), propertyName: null);
565561
}
566562

567-
internal void WriteV31Properties(IOpenApiWriter writer)
563+
internal void WriteJsonSchemaKeywords(IOpenApiWriter writer)
568564
{
569565
writer.WriteProperty(OpenApiConstants.Id, Id);
570566
writer.WriteProperty(OpenApiConstants.DollarSchema, Schema);
@@ -577,7 +573,7 @@ internal void WriteV31Properties(IOpenApiWriter writer)
577573
writer.WriteProperty(OpenApiConstants.V31ExclusiveMaximum, V31ExclusiveMaximum);
578574
writer.WriteProperty(OpenApiConstants.V31ExclusiveMinimum, V31ExclusiveMinimum);
579575
writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties, false);
580-
writer.WriteOptionalCollection(OpenApiConstants.Examples, _examples, (nodeWriter, s) => nodeWriter.WriteAny(s));
576+
writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (nodeWriter, s) => nodeWriter.WriteAny(s));
581577
writer.WriteOptionalMap(OpenApiConstants.PatternProperties, PatternProperties, (w, s) => s.SerializeAsV31(w));
582578
}
583579

@@ -820,15 +816,9 @@ private void SerializeTypeProperty(JsonSchemaType? type, IOpenApiWriter writer,
820816
}
821817
else
822818
{
823-
var list = new List<JsonSchemaType>();
824-
foreach (JsonSchemaType flag in jsonSchemaTypeValues)
825-
{
826-
if (type.Value.HasFlag(flag))
827-
{
828-
list.Add(flag);
829-
}
830-
}
831-
819+
var list = (from JsonSchemaType flag in jsonSchemaTypeValues
820+
where type.Value.HasFlag(flag)
821+
select flag).ToList();
832822
writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) => w.WriteValue(s.ToIdentifier()));
833823
}
834824
}
@@ -850,16 +840,9 @@ private void UpCastSchemaTypeToV31(JsonSchemaType? type, IOpenApiWriter writer)
850840
{
851841
// create a new array and insert the type and "null" as values
852842
Type = type | JsonSchemaType.Null;
853-
var list = new List<string>();
854-
foreach (JsonSchemaType? flag in jsonSchemaTypeValues)
855-
{
856-
// Check if the flag is set in 'type' using a bitwise AND operation
857-
if (Type.Value.HasFlag(flag))
858-
{
859-
list.Add(flag.ToIdentifier());
860-
}
861-
}
862-
843+
var list = (from JsonSchemaType? flag in jsonSchemaTypeValues// Check if the flag is set in 'type' using a bitwise AND operation
844+
where Type.Value.HasFlag(flag)
845+
select flag.ToIdentifier()).ToList();
863846
writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) => w.WriteValue(s));
864847
}
865848

src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using Microsoft.OpenApi.Reader.ParseNodes;
88
using System.Collections.Generic;
99
using System.Globalization;
10+
using System.Linq;
11+
using System.Text.Json.Nodes;
1012

1113
namespace Microsoft.OpenApi.Reader.V31
1214
{
@@ -254,7 +256,17 @@ public static OpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocum
254256

255257
foreach (var propertyNode in mapNode)
256258
{
257-
propertyNode.ParseField(schema, _openApiSchemaFixedFields, _openApiSchemaPatternFields);
259+
bool isRecognized = _openApiSchemaFixedFields.ContainsKey(propertyNode.Name) ||
260+
_openApiSchemaPatternFields.Any(p => p.Key(propertyNode.Name));
261+
262+
if (isRecognized)
263+
{
264+
propertyNode.ParseField(schema, _openApiSchemaFixedFields, _openApiSchemaPatternFields);
265+
}
266+
else
267+
{
268+
schema.UnrecognizedKeywords[propertyNode.Name] = propertyNode.JsonNode;
269+
}
258270
}
259271

260272
if (schema.Extensions.ContainsKey(OpenApiConstants.NullableExtension))

src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections;
66
using System.Collections.Generic;
77
using System.Linq;
8+
using System.Text.Json.Nodes;
89
using Microsoft.OpenApi.Interfaces;
910

1011
namespace Microsoft.OpenApi.Writers
@@ -253,6 +254,25 @@ public static void WriteRequiredMap(
253254
writer.WriteMapInternal(name, elements, action);
254255
}
255256

257+
/// <summary>
258+
/// Write the optional Open API element map (string to string mapping).
259+
/// </summary>
260+
/// <param name="writer">The Open API writer.</param>
261+
/// <param name="name">The property name.</param>
262+
/// <param name="elements">The map values.</param>
263+
/// <param name="action">The map element writer action.</param>
264+
public static void WriteOptionalMap(
265+
this IOpenApiWriter writer,
266+
string name,
267+
IDictionary<string, JsonNode> elements,
268+
Action<IOpenApiWriter, JsonNode> action)
269+
{
270+
if (elements != null && elements.Any())
271+
{
272+
writer.WriteMapInternal(name, elements, action);
273+
}
274+
}
275+
256276
/// <summary>
257277
/// Write the optional Open API element map (string to string mapping).
258278
/// </summary>

test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,5 +495,21 @@ public void ParseSchemaWithConstWorks()
495495
var schemaString = writer.ToString();
496496
schemaString.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
497497
}
498+
499+
[Fact]
500+
public void ParseSchemaWithUnrecognizedKeywordsWorks()
501+
{
502+
var input = @"{
503+
""type"": ""string"",
504+
""format"": ""date-time"",
505+
""customKeyword"": ""customValue"",
506+
""anotherKeyword"": 42,
507+
""x-test"": ""test""
508+
}
509+
";
510+
var schema = OpenApiModelFactory.Parse<OpenApiSchema>(input, OpenApiSpecVersion.OpenApi3_1, out _, "json");
511+
schema.UnrecognizedKeywords.Should().HaveCount(2);
512+
}
513+
498514
}
499515
}

test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/schemaWithJsonSchemaKeywords.yaml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,3 @@ required:
2828
- name
2929

3030
$dynamicAnchor: "addressDef"
31-
definitions:
32-
address:
33-
$dynamicAnchor: "addressDef"
34-
type: "object"
35-
properties:
36-
street:
37-
type: "string"
38-
city:
39-
type: "string"
40-
postalCode:
41-
type: "string"

test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ namespace Microsoft.OpenApi.Tests.Models
2222
[Collection("DefaultSettings")]
2323
public class OpenApiSchemaTests
2424
{
25-
public static OpenApiSchema BasicSchema = new();
25+
private static readonly OpenApiSchema BasicSchema = new();
2626

2727
public static readonly OpenApiSchema AdvancedSchemaNumber = new()
2828
{
@@ -602,15 +602,42 @@ public void OpenApiWalkerVisitsOpenApiSchemaNot()
602602
// Assert
603603
visitor.Titles.Count.Should().Be(2);
604604
}
605-
}
606605

607-
internal class SchemaVisitor : OpenApiVisitorBase
608-
{
609-
public List<string> Titles = new();
606+
[Fact]
607+
public void SerializeSchemaWithUnrecognizedPropertiesWorks()
608+
{
609+
// Arrange
610+
var schema = new OpenApiSchema
611+
{
612+
UnrecognizedKeywords = new Dictionary<string, JsonNode>()
613+
{
614+
["customKeyWord"] = "bar",
615+
["anotherKeyword"] = 42
616+
}
617+
};
610618

611-
public override void Visit(OpenApiSchema schema)
619+
var expected = @"{
620+
""unrecognizedKeywords"": {
621+
""customKeyWord"": ""bar"",
622+
""anotherKeyword"": 42
623+
}
624+
}";
625+
626+
// Act
627+
var actual = schema.SerializeAsJson(OpenApiSpecVersion.OpenApi3_1);
628+
629+
// Assert
630+
actual.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
631+
}
632+
633+
internal class SchemaVisitor : OpenApiVisitorBase
612634
{
613-
Titles.Add(schema.Title);
635+
public List<string> Titles = new();
636+
637+
public override void Visit(OpenApiSchema schema)
638+
{
639+
Titles.Add(schema.Title);
640+
}
614641
}
615642
}
616643
}

test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ namespace Microsoft.OpenApi.Models
512512
public const string Type = "type";
513513
public const string UnevaluatedProperties = "unevaluatedProperties";
514514
public const string UniqueItems = "uniqueItems";
515+
public const string UnrecognizedKeywords = "unrecognizedKeywords";
515516
public const string Url = "url";
516517
public const string V2ReferenceUri = "https://registry/definitions/";
517518
public const string V31ExclusiveMaximum = "exclusiveMaximum";
@@ -925,6 +926,7 @@ namespace Microsoft.OpenApi.Models
925926
public virtual bool UnEvaluatedProperties { get; set; }
926927
public virtual bool UnevaluatedProperties { get; set; }
927928
public virtual bool? UniqueItems { get; set; }
929+
public virtual System.Collections.Generic.IDictionary<string, System.Text.Json.Nodes.JsonNode> UnrecognizedKeywords { get; set; }
928930
public virtual bool UnresolvedReference { get; set; }
929931
public virtual decimal? V31ExclusiveMaximum { get; set; }
930932
public virtual decimal? V31ExclusiveMinimum { get; set; }
@@ -1856,6 +1858,7 @@ namespace Microsoft.OpenApi.Writers
18561858
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) { }
18571859
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) { }
18581860
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) { }
1861+
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) { }
18591862
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)
18601863
where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { }
18611864
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)

0 commit comments

Comments
 (0)