Skip to content

Commit 6987f7b

Browse files
Merge pull request #1899 from microsoft/mk/remove-recursive-keywords
OpenApiSchema refactor to remove recursive keywords
2 parents 5c35c7b + f45de4c commit 6987f7b

File tree

8 files changed

+131
-49
lines changed

8 files changed

+131
-49
lines changed

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class OpenApiSchema : IOpenApiAnnotatable, IOpenApiExtensible, IOpenApiRe
4343
/// <summary>
4444
/// $vocabulary- used in meta-schemas to identify the vocabularies available for use in schemas described by that meta-schema.
4545
/// </summary>
46-
public virtual string Vocabulary { get; set; }
46+
public virtual IDictionary<string, bool> Vocabulary { get; set; }
4747

4848
/// <summary>
4949
/// $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
5555
/// </summary>
5656
public virtual string DynamicAnchor { get; set; }
5757

58-
/// <summary>
59-
/// $recursiveAnchor - used to construct recursive schemas i.e one that has a reference to its own root, identified by the empty fragment URI reference ("#")
60-
/// </summary>
61-
public virtual string RecursiveAnchor { get; set; }
62-
63-
/// <summary>
64-
/// $recursiveRef - used to construct recursive schemas i.e one that has a reference to its own root, identified by the empty fragment URI reference ("#")
65-
/// </summary>
66-
public virtual string RecursiveRef { get; set; }
67-
6858
/// <summary>
6959
/// $defs - reserves a location for schema authors to inline re-usable JSON Schemas into a more general schema.
7060
/// The keyword does not directly affect the validation result
@@ -358,11 +348,9 @@ public OpenApiSchema(OpenApiSchema schema)
358348
Id = schema?.Id ?? Id;
359349
Schema = schema?.Schema ?? Schema;
360350
Comment = schema?.Comment ?? Comment;
361-
Vocabulary = schema?.Vocabulary ?? Vocabulary;
351+
Vocabulary = schema?.Vocabulary != null ? new Dictionary<string, bool>(schema.Vocabulary) : null;
362352
DynamicAnchor = schema?.DynamicAnchor ?? DynamicAnchor;
363353
DynamicRef = schema?.DynamicRef ?? DynamicRef;
364-
RecursiveAnchor = schema?.RecursiveAnchor ?? RecursiveAnchor;
365-
RecursiveRef = schema?.RecursiveRef ?? RecursiveRef;
366354
Definitions = schema?.Definitions != null ? new Dictionary<string, OpenApiSchema>(schema.Definitions) : null;
367355
UnevaluatedProperties = schema?.UnevaluatedProperties ?? UnevaluatedProperties;
368356
V31ExclusiveMaximum = schema?.V31ExclusiveMaximum ?? V31ExclusiveMaximum;
@@ -490,30 +478,30 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
490478
SerializeTypeProperty(Type, writer, version);
491479

492480
// allOf
493-
writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, (w, s) => s.SerializeAsV3(w));
481+
writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, callback);
494482

495483
// anyOf
496-
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, AnyOf, (w, s) => s.SerializeAsV3(w));
484+
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, AnyOf, callback);
497485

498486
// oneOf
499-
writer.WriteOptionalCollection(OpenApiConstants.OneOf, OneOf, (w, s) => s.SerializeAsV3(w));
487+
writer.WriteOptionalCollection(OpenApiConstants.OneOf, OneOf, callback);
500488

501489
// not
502-
writer.WriteOptionalObject(OpenApiConstants.Not, Not, (w, s) => s.SerializeAsV3(w));
490+
writer.WriteOptionalObject(OpenApiConstants.Not, Not, callback);
503491

504492
// items
505-
writer.WriteOptionalObject(OpenApiConstants.Items, Items, (w, s) => s.SerializeAsV3(w));
493+
writer.WriteOptionalObject(OpenApiConstants.Items, Items, callback);
506494

507495
// properties
508-
writer.WriteOptionalMap(OpenApiConstants.Properties, Properties, (w, s) => s.SerializeAsV3(w));
496+
writer.WriteOptionalMap(OpenApiConstants.Properties, Properties, callback);
509497

510498
// additionalProperties
511499
if (AdditionalPropertiesAllowed)
512500
{
513501
writer.WriteOptionalObject(
514502
OpenApiConstants.AdditionalProperties,
515503
AdditionalProperties,
516-
(w, s) => s.SerializeAsV3(w));
504+
callback);
517505
}
518506
else
519507
{
@@ -536,7 +524,7 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
536524
}
537525

538526
// discriminator
539-
writer.WriteOptionalObject(OpenApiConstants.Discriminator, Discriminator, (w, s) => s.SerializeAsV3(w));
527+
writer.WriteOptionalObject(OpenApiConstants.Discriminator, Discriminator, callback);
540528

541529
// readOnly
542530
writer.WriteProperty(OpenApiConstants.ReadOnly, ReadOnly, false);
@@ -548,7 +536,7 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
548536
writer.WriteOptionalObject(OpenApiConstants.Xml, Xml, (w, s) => s.SerializeAsV2(w));
549537

550538
// externalDocs
551-
writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, (w, s) => s.SerializeAsV3(w));
539+
writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, callback);
552540

553541
// example
554542
writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e));
@@ -557,7 +545,7 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
557545
writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated, false);
558546

559547
// extensions
560-
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0);
548+
writer.WriteExtensions(Extensions, version);
561549

562550
writer.WriteEndObject();
563551
}
@@ -574,12 +562,10 @@ internal void WriteV31Properties(IOpenApiWriter writer)
574562
writer.WriteProperty(OpenApiConstants.Id, Id);
575563
writer.WriteProperty(OpenApiConstants.DollarSchema, Schema);
576564
writer.WriteProperty(OpenApiConstants.Comment, Comment);
577-
writer.WriteProperty(OpenApiConstants.Vocabulary, Vocabulary);
578-
writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, (w, s) => s.SerializeAsV3(w));
565+
writer.WriteOptionalMap(OpenApiConstants.Vocabulary, Vocabulary, (w, s) => w.WriteValue(s));
566+
writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, (w, s) => s.SerializeAsV31(w));
579567
writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef);
580568
writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor);
581-
writer.WriteProperty(OpenApiConstants.RecursiveAnchor, RecursiveAnchor);
582-
writer.WriteProperty(OpenApiConstants.RecursiveRef, RecursiveRef);
583569
writer.WriteProperty(OpenApiConstants.V31ExclusiveMaximum, V31ExclusiveMaximum);
584570
writer.WriteProperty(OpenApiConstants.V31ExclusiveMinimum, V31ExclusiveMinimum);
585571
writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties, false);

src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,12 @@ internal OpenApiSchemaReference(OpenApiSchema target, string referenceId)
7474
/// <inheritdoc/>
7575
public override string Comment { get => Target.Comment; set => Target.Comment = value; }
7676
/// <inheritdoc/>
77-
public override string Vocabulary { get => Target.Vocabulary; set => Target.Vocabulary = value; }
77+
public override IDictionary<string, bool> Vocabulary { get => Target.Vocabulary; set => Target.Vocabulary = value; }
7878
/// <inheritdoc/>
7979
public override string DynamicRef { get => Target.DynamicRef; set => Target.DynamicRef = value; }
8080
/// <inheritdoc/>
8181
public override string DynamicAnchor { get => Target.DynamicAnchor; set => Target.DynamicAnchor = value; }
8282
/// <inheritdoc/>
83-
public override string RecursiveAnchor { get => Target.RecursiveAnchor; set => Target.RecursiveAnchor = value; }
84-
/// <inheritdoc/>
85-
public override string RecursiveRef { get => Target.RecursiveRef; set => Target.RecursiveRef = value; }
86-
/// <inheritdoc/>
8783
public override IDictionary<string, OpenApiSchema> Definitions { get => Target.Definitions; set => Target.Definitions = value; }
8884
/// <inheritdoc/>
8985
public override decimal? V31ExclusiveMaximum { get => Target.V31ExclusiveMaximum; set => Target.V31ExclusiveMaximum = value; }

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ internal static partial class OpenApiV31Deserializer
3232
},
3333
{
3434
"$vocabulary",
35-
(o, n, _) => o.Vocabulary = n.GetScalarValue()
35+
(o, n, _) => o.Vocabulary = n.CreateSimpleMap(LoadBool)
3636
},
3737
{
3838
"$dynamicRef",
@@ -42,14 +42,6 @@ internal static partial class OpenApiV31Deserializer
4242
"$dynamicAnchor",
4343
(o, n, _) => o.DynamicAnchor = n.GetScalarValue()
4444
},
45-
{
46-
"$recursiveAnchor",
47-
(o, n, _) => o.RecursiveAnchor = n.GetScalarValue()
48-
},
49-
{
50-
"$recursiveRef",
51-
(o, n, _) => o.RecursiveRef = n.GetScalarValue()
52-
},
5345
{
5446
"$defs",
5547
(o, n, t) => o.Definitions = n.CreateMap(LoadSchema, t)

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ private static string LoadString(ParseNode node)
145145
return node.GetScalarValue();
146146
}
147147

148+
private static bool LoadBool(ParseNode node)
149+
{
150+
return bool.Parse(node.GetScalarValue());
151+
}
152+
148153
private static (string, string) GetReferenceIdAndExternalResource(string pointer)
149154
{
150155
/* Check whether the reference pointer is a URL

src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,25 @@ public static void WriteOptionalMap(
272272
}
273273
}
274274

275+
/// <summary>
276+
/// Write the optional Open API element map (string to string mapping).
277+
/// </summary>
278+
/// <param name="writer">The Open API writer.</param>
279+
/// <param name="name">The property name.</param>
280+
/// <param name="elements">The map values.</param>
281+
/// <param name="action">The map element writer action.</param>
282+
public static void WriteOptionalMap(
283+
this IOpenApiWriter writer,
284+
string name,
285+
IDictionary<string, bool> elements,
286+
Action<IOpenApiWriter, bool> action)
287+
{
288+
if (elements != null && elements.Any())
289+
{
290+
writer.WriteMapInternal(name, elements, action);
291+
}
292+
}
293+
275294
/// <summary>
276295
/// Write the optional Open API element map.
277296
/// </summary>

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

44
using System.Collections.Generic;
@@ -404,5 +404,51 @@ public void LoadSchemaWithNullableExtensionAsV31Works(string filePath)
404404
// Assert
405405
schema.Type.Should().BeEquivalentTo(new string[] { "string", "null" });
406406
}
407+
408+
[Fact]
409+
public void SerializeSchemaWithJsonSchemaKeywordsWorks()
410+
{
411+
// Arrange
412+
var expected = @"$id: https://example.com/schemas/person.schema.yaml
413+
$schema: https://json-schema.org/draft/2020-12/schema
414+
$comment: A schema defining a person object with optional references to dynamic components.
415+
$vocabulary:
416+
https://json-schema.org/draft/2020-12/vocab/core: true
417+
https://json-schema.org/draft/2020-12/vocab/applicator: true
418+
https://json-schema.org/draft/2020-12/vocab/validation: true
419+
https://json-schema.org/draft/2020-12/vocab/meta-data: false
420+
https://json-schema.org/draft/2020-12/vocab/format-annotation: false
421+
$dynamicAnchor: addressDef
422+
title: Person
423+
required:
424+
- name
425+
type: object
426+
properties:
427+
name:
428+
$comment: The person's full name
429+
type: string
430+
age:
431+
$comment: Age must be a non-negative integer
432+
minimum: 0
433+
type: integer
434+
address:
435+
$comment: Reference to an address definition which can change dynamically
436+
$dynamicRef: '#addressDef'
437+
description: Schema for a person object
438+
";
439+
var path = Path.Combine(SampleFolderPath, "schemaWithJsonSchemaKeywords.yaml");
440+
441+
// Act
442+
var schema = OpenApiModelFactory.Load<OpenApiSchema>(path, OpenApiSpecVersion.OpenApi3_1, out _);
443+
444+
// serialization
445+
var writer = new StringWriter();
446+
schema.SerializeAsV31(new OpenApiYamlWriter(writer));
447+
var schemaString = writer.ToString();
448+
449+
// Assert
450+
schema.Vocabulary.Keys.Count.Should().Be(5);
451+
schemaString.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
452+
}
407453
}
408454
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
$schema: "https://json-schema.org/draft/2020-12/schema"
2+
$id: "https://example.com/schemas/person.schema.yaml"
3+
$comment: "A schema defining a person object with optional references to dynamic components."
4+
$vocabulary:
5+
"https://json-schema.org/draft/2020-12/vocab/core": true
6+
"https://json-schema.org/draft/2020-12/vocab/applicator": true
7+
"https://json-schema.org/draft/2020-12/vocab/validation": true
8+
"https://json-schema.org/draft/2020-12/vocab/meta-data": false
9+
"https://json-schema.org/draft/2020-12/vocab/format-annotation": false
10+
11+
title: "Person"
12+
description: "Schema for a person object"
13+
type: "object"
14+
15+
properties:
16+
name:
17+
type: "string"
18+
$comment: "The person's full name"
19+
age:
20+
type: "integer"
21+
minimum: 0
22+
$comment: "Age must be a non-negative integer"
23+
address:
24+
$dynamicRef: "#addressDef"
25+
$comment: "Reference to an address definition which can change dynamically"
26+
27+
required:
28+
- name
29+
30+
$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/PublicApi/PublicApi.approved.txt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -901,8 +901,6 @@ namespace Microsoft.OpenApi.Models
901901
public virtual System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.Models.OpenApiSchema> PatternProperties { get; set; }
902902
public virtual System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.Models.OpenApiSchema> Properties { get; set; }
903903
public virtual bool ReadOnly { get; set; }
904-
public virtual string RecursiveAnchor { get; set; }
905-
public virtual string RecursiveRef { get; set; }
906904
public virtual Microsoft.OpenApi.Models.OpenApiReference Reference { get; set; }
907905
public virtual System.Collections.Generic.ISet<string> Required { get; set; }
908906
public virtual string Schema { get; set; }
@@ -914,7 +912,7 @@ namespace Microsoft.OpenApi.Models
914912
public virtual bool UnresolvedReference { get; set; }
915913
public virtual decimal? V31ExclusiveMaximum { get; set; }
916914
public virtual decimal? V31ExclusiveMinimum { get; set; }
917-
public virtual string Vocabulary { get; set; }
915+
public virtual System.Collections.Generic.IDictionary<string, bool> Vocabulary { get; set; }
918916
public virtual bool WriteOnly { get; set; }
919917
public virtual Microsoft.OpenApi.Models.OpenApiXml Xml { get; set; }
920918
public virtual void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { }
@@ -1243,8 +1241,6 @@ namespace Microsoft.OpenApi.Models.References
12431241
public override System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.Models.OpenApiSchema> PatternProperties { get; set; }
12441242
public override System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.Models.OpenApiSchema> Properties { get; set; }
12451243
public override bool ReadOnly { get; set; }
1246-
public override string RecursiveAnchor { get; set; }
1247-
public override string RecursiveRef { get; set; }
12481244
public override System.Collections.Generic.ISet<string> Required { get; set; }
12491245
public override string Schema { get; set; }
12501246
public override string Title { get; set; }
@@ -1254,7 +1250,7 @@ namespace Microsoft.OpenApi.Models.References
12541250
public override bool? UniqueItems { get; set; }
12551251
public override decimal? V31ExclusiveMaximum { get; set; }
12561252
public override decimal? V31ExclusiveMinimum { get; set; }
1257-
public override string Vocabulary { get; set; }
1253+
public override System.Collections.Generic.IDictionary<string, bool> Vocabulary { get; set; }
12581254
public override bool WriteOnly { get; set; }
12591255
public override Microsoft.OpenApi.Models.OpenApiXml Xml { get; set; }
12601256
public override void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { }
@@ -1849,6 +1845,7 @@ namespace Microsoft.OpenApi.Writers
18491845
{
18501846
public static void WriteOptionalCollection(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IEnumerable<string> elements, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, string> action) { }
18511847
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) { }
1848+
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) { }
18521849
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) { }
18531850
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)
18541851
where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { }

0 commit comments

Comments
 (0)