Skip to content

Commit 8fe63fe

Browse files
authored
Merge pull request #2376 from microsoft/copilot/fix-2369
Add JSON Schema 2020-12 metadata annotations to OpenApiSchemaReference
2 parents 71d9072 + 6e12152 commit 8fe63fe

File tree

53 files changed

+1348
-208
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1348
-208
lines changed

src/Microsoft.OpenApi/Interfaces/IOpenApiReferenceHolder.cs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,35 @@ namespace Microsoft.OpenApi
77
/// A generic interface for OpenApiReferenceable objects that have a target.
88
/// </summary>
99
/// <typeparam name="T">The type of the target being referenced</typeparam>
10-
/// <typeparam name="V">The type of the interface implemented by both the target and the reference type</typeparam>
11-
public interface IOpenApiReferenceHolder<out T, V> : IOpenApiReferenceHolder where T : IOpenApiReferenceable, V
10+
/// <typeparam name="U">The type of the interface implemented by both the target and the reference type</typeparam>
11+
/// <typeparam name="V">The type for the reference holding the additional fields and annotations</typeparam>
12+
public interface IOpenApiReferenceHolder<out T, U, V> : IOpenApiReferenceHolder<V> where T : IOpenApiReferenceable, U where V : BaseOpenApiReference, new()
1213
{
1314
/// <summary>
1415
/// Gets the resolved target object.
1516
/// </summary>
16-
V? Target { get; }
17-
17+
U? Target { get; }
18+
1819
/// <summary>
1920
/// Gets the recursively resolved target object.
2021
/// </summary>
2122
T? RecursiveTarget { get; }
22-
23+
2324
/// <summary>
2425
/// Copy the reference as a target element with overrides.
2526
/// </summary>
26-
V CopyReferenceAsTargetElementWithOverrides(V source);
27+
U CopyReferenceAsTargetElementWithOverrides(U source);
28+
}
29+
/// <summary>
30+
/// A generic interface for OpenApiReferenceable objects that have a target.
31+
/// </summary>
32+
/// <typeparam name="V">The type for the reference holding the additional fields and annotations</typeparam>
33+
public interface IOpenApiReferenceHolder<V> : IOpenApiReferenceHolder where V : BaseOpenApiReference, new()
34+
{
35+
/// <summary>
36+
/// Reference object.
37+
/// </summary>
38+
V Reference { get; init; }
2739
}
2840
/// <summary>
2941
/// A generic interface for OpenApiReferenceable objects that have a target.
@@ -34,10 +46,5 @@ public interface IOpenApiReferenceHolder : IOpenApiSerializable
3446
/// Indicates if object is populated with data or is just a reference to the data
3547
/// </summary>
3648
bool UnresolvedReference { get; }
37-
38-
/// <summary>
39-
/// Reference object.
40-
/// </summary>
41-
OpenApiReference Reference { get; init; }
4249
}
4350
}

src/Microsoft.OpenApi/Models/OpenApiReference.cs renamed to src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs

Lines changed: 40 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,8 @@ namespace Microsoft.OpenApi
1111
/// <summary>
1212
/// A simple object to allow referencing other components in the specification, internally and externally.
1313
/// </summary>
14-
public class OpenApiReference : IOpenApiSerializable, IOpenApiDescribedElement, IOpenApiSummarizedElement
14+
public class BaseOpenApiReference : IOpenApiSerializable
1515
{
16-
/// <summary>
17-
/// A short summary which by default SHOULD override that of the referenced component.
18-
/// If the referenced object-type does not allow a summary field, then this field has no effect.
19-
/// </summary>
20-
public string? Summary { get; set; }
21-
22-
/// <summary>
23-
/// A description which by default SHOULD override that of the referenced component.
24-
/// CommonMark syntax MAY be used for rich text representation.
25-
/// If the referenced object-type does not allow a description field, then this field has no effect.
26-
/// </summary>
27-
public string? Description { get; set; }
28-
2916
/// <summary>
3017
/// External resource in the reference.
3118
/// It maybe:
@@ -143,45 +130,43 @@ public string? ReferenceV2
143130
/// <summary>
144131
/// Parameterless constructor
145132
/// </summary>
146-
public OpenApiReference() { }
133+
public BaseOpenApiReference() { }
147134

148135
/// <summary>
149-
/// Initializes a copy instance of the <see cref="OpenApiReference"/> object
136+
/// Initializes a copy instance of the <see cref="BaseOpenApiReference"/> object
150137
/// </summary>
151-
public OpenApiReference(OpenApiReference reference)
138+
public BaseOpenApiReference(BaseOpenApiReference reference)
152139
{
153140
Utils.CheckArgumentNull(reference);
154-
Summary = reference.Summary;
155-
Description = reference.Description;
156141
ExternalResource = reference.ExternalResource;
157142
Type = reference.Type;
158143
Id = reference.Id;
159144
HostDocument = reference.HostDocument;
160145
}
161146

162-
/// <summary>
163-
/// Serialize <see cref="OpenApiReference"/> to Open Api v3.1.
164-
/// </summary>
165-
public void SerializeAsV31(IOpenApiWriter writer)
147+
/// <inheritdoc/>
148+
public virtual void SerializeAsV31(IOpenApiWriter writer)
166149
{
167-
SerializeInternal(writer, w =>
168-
{
169-
// summary and description are in 3.1 but not in 3.0
170-
w.WriteProperty(OpenApiConstants.Summary, Summary);
171-
w.WriteProperty(OpenApiConstants.Description, Description);
172-
});
150+
SerializeInternal(writer, SerializeAdditionalV31Properties);
173151
}
174152

175153
/// <summary>
176-
/// Serialize <see cref="OpenApiReference"/> to Open Api v3.0.
154+
/// Serialize additional properties for Open Api v3.1.
177155
/// </summary>
178-
public void SerializeAsV3(IOpenApiWriter writer)
156+
/// <param name="writer"></param>
157+
protected virtual void SerializeAdditionalV31Properties(IOpenApiWriter writer)
158+
{
159+
// noop for the base type
160+
}
161+
162+
/// <inheritdoc/>
163+
public virtual void SerializeAsV3(IOpenApiWriter writer)
179164
{
180165
SerializeInternal(writer);
181166
}
182167

183168
/// <summary>
184-
/// Serialize <see cref="OpenApiReference"/>
169+
/// Serialize <see cref="BaseOpenApiReference"/>
185170
/// </summary>
186171
private void SerializeInternal(IOpenApiWriter writer, Action<IOpenApiWriter>? callback = null)
187172
{
@@ -206,10 +191,8 @@ private void SerializeInternal(IOpenApiWriter writer, Action<IOpenApiWriter>? ca
206191
writer.WriteEndObject();
207192
}
208193

209-
/// <summary>
210-
/// Serialize <see cref="OpenApiReference"/> to Open Api v2.0.
211-
/// </summary>
212-
public void SerializeAsV2(IOpenApiWriter writer)
194+
/// <inheritdoc/>
195+
public virtual void SerializeAsV2(IOpenApiWriter writer)
213196
{
214197
Utils.CheckArgumentNull(writer);
215198

@@ -291,23 +274,27 @@ internal void EnsureHostDocumentIsSet(OpenApiDocument currentDocument)
291274
Utils.CheckArgumentNull(currentDocument);
292275
hostDocument ??= currentDocument;
293276
}
294-
private static string? GetPropertyValueFromNode(JsonObject jsonObject, string key) =>
277+
/// <summary>
278+
/// Gets the property value from a JsonObject node.
279+
/// </summary>
280+
/// <param name="jsonObject">The object to get the value from</param>
281+
/// <param name="key">The key of the property</param>
282+
/// <returns>The property value</returns>
283+
protected internal static string? GetPropertyValueFromNode(JsonObject jsonObject, string key) =>
295284
jsonObject.TryGetPropertyValue(key, out var valueNode) && valueNode is JsonValue valueCast && valueCast.TryGetValue<string>(out var strValue) ? strValue : null;
296-
internal void SetSummaryAndDescriptionFromMapNode(MapNode mapNode)
285+
internal virtual void SetMetadataFromMapNode(MapNode mapNode)
297286
{
298-
var (description, summary) = mapNode.JsonNode switch {
299-
JsonObject jsonObject => (GetPropertyValueFromNode(jsonObject, OpenApiConstants.Description),
300-
GetPropertyValueFromNode(jsonObject, OpenApiConstants.Summary)),
301-
_ => (null, null)
302-
};
303-
if (!string.IsNullOrEmpty(description))
304-
{
305-
Description = description;
306-
}
307-
if (!string.IsNullOrEmpty(summary))
308-
{
309-
Summary = summary;
310-
}
287+
if (mapNode.JsonNode is not JsonObject jsonObject) return;
288+
SetAdditional31MetadataFromMapNode(jsonObject);
289+
}
290+
291+
/// <summary>
292+
/// Sets additional metadata from the map node.
293+
/// </summary>
294+
/// <param name="jsonObject">The object to get the data from</param>
295+
protected virtual void SetAdditional31MetadataFromMapNode(JsonObject jsonObject)
296+
{
297+
// noop for the base type
311298
}
312299

313300
internal void SetJsonPointerPath(string pointer, string nodeLocation)
@@ -319,11 +306,11 @@ internal void SetJsonPointerPath(string pointer, string nodeLocation)
319306
}
320307

321308
// Absolute reference or anchor (e.g. "#/components/schemas/..." or full URL)
322-
else if ((pointer.Contains('#') || pointer.StartsWith("http", StringComparison.OrdinalIgnoreCase))
309+
else if ((pointer.Contains('#') || pointer.StartsWith("http", StringComparison.OrdinalIgnoreCase))
323310
&& !string.Equals(ReferenceV3, pointer, StringComparison.OrdinalIgnoreCase))
324311
{
325312
ReferenceV3 = pointer;
326-
}
313+
}
327314
}
328315

329316
private static string ResolveRelativePointer(string nodeLocation, string relativeRef)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Text.Json.Nodes;
8+
9+
namespace Microsoft.OpenApi;
10+
11+
/// <summary>
12+
/// Schema reference information that includes metadata annotations from JSON Schema 2020-12.
13+
/// This class extends OpenApiReference to provide schema-specific metadata override capabilities.
14+
/// </summary>
15+
public class JsonSchemaReference : OpenApiReferenceWithDescription
16+
{
17+
/// <summary>
18+
/// A default value which by default SHOULD override that of the referenced component.
19+
/// If the referenced object-type does not allow a default field, then this field has no effect.
20+
/// </summary>
21+
public JsonNode? Default { get; set; }
22+
23+
/// <summary>
24+
/// A title which by default SHOULD override that of the referenced component.
25+
/// If the referenced object-type does not allow a title field, then this field has no effect.
26+
/// </summary>
27+
public string? Title { get; set; }
28+
29+
/// <summary>
30+
/// Indicates whether the referenced component is deprecated.
31+
/// If the referenced object-type does not allow a deprecated field, then this field has no effect.
32+
/// </summary>
33+
public bool? Deprecated { get; set; }
34+
35+
/// <summary>
36+
/// Indicates whether the referenced component is read-only.
37+
/// If the referenced object-type does not allow a readOnly field, then this field has no effect.
38+
/// </summary>
39+
public bool? ReadOnly { get; set; }
40+
41+
/// <summary>
42+
/// Indicates whether the referenced component is write-only.
43+
/// If the referenced object-type does not allow a writeOnly field, then this field has no effect.
44+
/// </summary>
45+
public bool? WriteOnly { get; set; }
46+
47+
/// <summary>
48+
/// Example values which by default SHOULD override those of the referenced component.
49+
/// If the referenced object-type does not allow examples, then this field has no effect.
50+
/// </summary>
51+
public IList<JsonNode>? Examples { get; set; }
52+
53+
/// <summary>
54+
/// Parameterless constructor
55+
/// </summary>
56+
public JsonSchemaReference() { }
57+
58+
/// <summary>
59+
/// Initializes a copy instance of the <see cref="JsonSchemaReference"/> object
60+
/// </summary>
61+
public JsonSchemaReference(JsonSchemaReference reference) : base(reference)
62+
{
63+
Utils.CheckArgumentNull(reference);
64+
Default = reference.Default;
65+
Title = reference.Title;
66+
Deprecated = reference.Deprecated;
67+
ReadOnly = reference.ReadOnly;
68+
WriteOnly = reference.WriteOnly;
69+
Examples = reference.Examples;
70+
}
71+
72+
/// <inheritdoc/>
73+
protected override void SerializeAdditionalV31Properties(IOpenApiWriter writer)
74+
{
75+
if (Type != ReferenceType.Schema) throw new InvalidOperationException(
76+
$"JsonSchemaReference can only be serialized for ReferenceType.Schema, but was {Type}.");
77+
78+
base.SerializeAdditionalV31Properties(writer);
79+
// Additional schema metadata annotations in 3.1
80+
writer.WriteOptionalObject(OpenApiConstants.Default, Default, (w, d) => w.WriteAny(d));
81+
writer.WriteProperty(OpenApiConstants.Title, Title);
82+
if (Deprecated.HasValue)
83+
{
84+
writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated.Value, false);
85+
}
86+
if (ReadOnly.HasValue)
87+
{
88+
writer.WriteProperty(OpenApiConstants.ReadOnly, ReadOnly.Value, false);
89+
}
90+
if (WriteOnly.HasValue)
91+
{
92+
writer.WriteProperty(OpenApiConstants.WriteOnly, WriteOnly.Value, false);
93+
}
94+
if (Examples != null && Examples.Any())
95+
{
96+
writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (w, e) => w.WriteAny(e));
97+
}
98+
}
99+
100+
/// <inheritdoc/>
101+
protected override void SetAdditional31MetadataFromMapNode(JsonObject jsonObject)
102+
{
103+
base.SetAdditional31MetadataFromMapNode(jsonObject);
104+
105+
var title = GetPropertyValueFromNode(jsonObject, OpenApiConstants.Title);
106+
if (!string.IsNullOrEmpty(title))
107+
{
108+
Title = title;
109+
}
110+
111+
// Boolean properties
112+
if (jsonObject.TryGetPropertyValue(OpenApiConstants.Deprecated, out var deprecatedNode) && deprecatedNode is JsonValue deprecatedValue && deprecatedValue.TryGetValue<bool>(out var deprecated))
113+
{
114+
Deprecated = deprecated;
115+
}
116+
117+
if (jsonObject.TryGetPropertyValue(OpenApiConstants.ReadOnly, out var readOnlyNode) && readOnlyNode is JsonValue readOnlyValue && readOnlyValue.TryGetValue<bool>(out var readOnly))
118+
{
119+
ReadOnly = readOnly;
120+
}
121+
122+
if (jsonObject.TryGetPropertyValue(OpenApiConstants.WriteOnly, out var writeOnlyNode) && writeOnlyNode is JsonValue writeOnlyValue && writeOnlyValue.TryGetValue<bool>(out var writeOnly))
123+
{
124+
WriteOnly = writeOnly;
125+
}
126+
127+
// Default value
128+
if (jsonObject.TryGetPropertyValue(OpenApiConstants.Default, out var defaultNode))
129+
{
130+
Default = defaultNode;
131+
}
132+
133+
// Examples
134+
if (jsonObject.TryGetPropertyValue(OpenApiConstants.Examples, out var examplesNode) && examplesNode is JsonArray examplesArray)
135+
{
136+
Examples = examplesArray.OfType<JsonNode>().ToList();
137+
}
138+
}
139+
}

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -496,9 +496,9 @@ public void SetReferenceHostDocument()
496496
}
497497

498498
/// <summary>
499-
/// Load the referenced <see cref="IOpenApiReferenceable"/> object from a <see cref="OpenApiReference"/> object
499+
/// Load the referenced <see cref="IOpenApiReferenceable"/> object from a <see cref="BaseOpenApiReference"/> object
500500
/// </summary>
501-
internal T? ResolveReferenceTo<T>(OpenApiReference reference) where T : IOpenApiReferenceable
501+
internal T? ResolveReferenceTo<T>(BaseOpenApiReference reference) where T : IOpenApiReferenceable
502502
{
503503

504504
if (ResolveReference(reference, reference.IsExternal) is T result)
@@ -564,9 +564,9 @@ private static string ConvertByteArrayToString(byte[] hash)
564564
}
565565

566566
/// <summary>
567-
/// Load the referenced <see cref="IOpenApiReferenceable"/> object from a <see cref="OpenApiReference"/> object
567+
/// Load the referenced <see cref="IOpenApiReferenceable"/> object from a <see cref="BaseOpenApiReference"/> object
568568
/// </summary>
569-
internal IOpenApiReferenceable? ResolveReference(OpenApiReference? reference, bool useExternal)
569+
internal IOpenApiReferenceable? ResolveReference(BaseOpenApiReference? reference, bool useExternal)
570570
{
571571
if (reference == null)
572572
{

0 commit comments

Comments
 (0)