Skip to content

Add JSON Schema 2020-12 metadata annotations to OpenApiSchemaReference #2376

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Jun 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ed6df9f
Initial plan for issue
Copilot Jun 4, 2025
7967256
Initial commit - fix build errors with string.Split ambiguity
Copilot Jun 4, 2025
73ec5a7
Add metadata annotations to OpenApiSchemaReference
Copilot Jun 4, 2025
66ccf74
Complete OpenApiSchemaReference annotations implementation
Copilot Jun 4, 2025
10cde39
Refactor schema metadata annotations to separate class per review fee…
Copilot Jun 4, 2025
b328774
chore: reverts useless changes from copilot
baywet Jun 5, 2025
ddf17fe
chore: linting
baywet Jun 5, 2025
f0802e5
fix: makes reference serialization object generic
baywet Jun 5, 2025
ffb083c
chore: cleans up interface definitions
baywet Jun 6, 2025
4139170
chore: fix implementation type definition
baywet Jun 6, 2025
0a686fd
chore: fixes the reference copy conundrum
baywet Jun 6, 2025
33cc238
chore: removes summary property from references that do not support it
baywet Jun 6, 2025
03659f7
fix: removes description field from references that do not support it
baywet Jun 6, 2025
e355808
chore: updates test validation information
baywet Jun 6, 2025
f74afbc
Potential fix for code scanning alert no. 2304: Missed opportunity to…
baywet Jun 6, 2025
18f91d0
chore: Apply suggestions from code review
baywet Jun 6, 2025
a37a871
chore: reverts undesired change from copilot
baywet Jun 6, 2025
449ab26
chore: refactoring
baywet Jun 6, 2025
9248560
fix: loading of header reference description
baywet Jun 6, 2025
86892b3
fix: callback reference annotations parsing
baywet Jun 6, 2025
8bf012b
fix: example reference annotation parsing
baywet Jun 6, 2025
2a62c5a
fix: link reference annotations parsing
baywet Jun 6, 2025
b1578f3
fix: parameter reference annoation parsing
baywet Jun 6, 2025
d31ed4c
fix: path item reference annoations parsing
baywet Jun 6, 2025
e455f52
fix: response reference annotations parsing
baywet Jun 6, 2025
d9a78dc
fix: request body reference annotations parsing
baywet Jun 6, 2025
ccc3733
fix: security scheme reference annoations parsing
baywet Jun 6, 2025
8ed4512
chore: adds unit tests for tags reference parsing
baywet Jun 6, 2025
6e12152
chore: adds a unit test for json schema ref annotations parsing
baywet Jun 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions src/Microsoft.OpenApi/Interfaces/IOpenApiReferenceHolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,35 @@
/// A generic interface for OpenApiReferenceable objects that have a target.
/// </summary>
/// <typeparam name="T">The type of the target being referenced</typeparam>
/// <typeparam name="V">The type of the interface implemented by both the target and the reference type</typeparam>
public interface IOpenApiReferenceHolder<out T, V> : IOpenApiReferenceHolder where T : IOpenApiReferenceable, V
/// <typeparam name="U">The type of the interface implemented by both the target and the reference type</typeparam>
/// <typeparam name="V">The type for the reference holding the additional fields and annotations</typeparam>
public interface IOpenApiReferenceHolder<out T, U, V> : IOpenApiReferenceHolder<V> where T : IOpenApiReferenceable, U where V : BaseOpenApiReference, new()

Check warning on line 12 in src/Microsoft.OpenApi/Interfaces/IOpenApiReferenceHolder.cs

View workflow job for this annotation

GitHub Actions / Build

Reduce the number of generic parameters in the 'IOpenApiReferenceHolder' interface to no more than the 2 authorized. (https://rules.sonarsource.com/csharp/RSPEC-2436)
{
/// <summary>
/// Gets the resolved target object.
/// </summary>
V? Target { get; }
U? Target { get; }

/// <summary>
/// Gets the recursively resolved target object.
/// </summary>
T? RecursiveTarget { get; }

/// <summary>
/// Copy the reference as a target element with overrides.
/// </summary>
V CopyReferenceAsTargetElementWithOverrides(V source);
U CopyReferenceAsTargetElementWithOverrides(U source);
}
/// <summary>
/// A generic interface for OpenApiReferenceable objects that have a target.
/// </summary>
/// <typeparam name="V">The type for the reference holding the additional fields and annotations</typeparam>
public interface IOpenApiReferenceHolder<V> : IOpenApiReferenceHolder where V : BaseOpenApiReference, new()
{
/// <summary>
/// Reference object.
/// </summary>
V Reference { get; init; }
}
/// <summary>
/// A generic interface for OpenApiReferenceable objects that have a target.
Expand All @@ -34,10 +46,5 @@
/// Indicates if object is populated with data or is just a reference to the data
/// </summary>
bool UnresolvedReference { get; }

/// <summary>
/// Reference object.
/// </summary>
OpenApiReference Reference { get; init; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,8 @@
/// <summary>
/// A simple object to allow referencing other components in the specification, internally and externally.
/// </summary>
public class OpenApiReference : IOpenApiSerializable, IOpenApiDescribedElement, IOpenApiSummarizedElement
public class BaseOpenApiReference : IOpenApiSerializable
{
/// <summary>
/// A short summary which by default SHOULD override that of the referenced component.
/// If the referenced object-type does not allow a summary field, then this field has no effect.
/// </summary>
public string? Summary { get; set; }

/// <summary>
/// A description which by default SHOULD override that of the referenced component.
/// CommonMark syntax MAY be used for rich text representation.
/// If the referenced object-type does not allow a description field, then this field has no effect.
/// </summary>
public string? Description { get; set; }

/// <summary>
/// External resource in the reference.
/// It maybe:
Expand Down Expand Up @@ -68,7 +55,7 @@
/// <summary>
/// The OpenApiDocument that is hosting the OpenApiReference instance. This is used to enable dereferencing the reference.
/// </summary>
public OpenApiDocument? HostDocument { get => hostDocument; init => hostDocument = value; }

Check warning on line 58 in src/Microsoft.OpenApi/Models/BaseOpenApiReference.cs

View workflow job for this annotation

GitHub Actions / Build

Make this an auto-implemented property and remove its backing field. (https://rules.sonarsource.com/csharp/RSPEC-2292)

private string? _referenceV3;
/// <summary>
Expand Down Expand Up @@ -143,45 +130,43 @@
/// <summary>
/// Parameterless constructor
/// </summary>
public OpenApiReference() { }
public BaseOpenApiReference() { }

/// <summary>
/// Initializes a copy instance of the <see cref="OpenApiReference"/> object
/// Initializes a copy instance of the <see cref="BaseOpenApiReference"/> object
/// </summary>
public OpenApiReference(OpenApiReference reference)
public BaseOpenApiReference(BaseOpenApiReference reference)
{
Utils.CheckArgumentNull(reference);
Summary = reference.Summary;
Description = reference.Description;
ExternalResource = reference.ExternalResource;
Type = reference.Type;
Id = reference.Id;
HostDocument = reference.HostDocument;
}

/// <summary>
/// Serialize <see cref="OpenApiReference"/> to Open Api v3.1.
/// </summary>
public void SerializeAsV31(IOpenApiWriter writer)
/// <inheritdoc/>
public virtual void SerializeAsV31(IOpenApiWriter writer)
{
SerializeInternal(writer, w =>
{
// summary and description are in 3.1 but not in 3.0
w.WriteProperty(OpenApiConstants.Summary, Summary);
w.WriteProperty(OpenApiConstants.Description, Description);
});
SerializeInternal(writer, SerializeAdditionalV31Properties);
}

/// <summary>
/// Serialize <see cref="OpenApiReference"/> to Open Api v3.0.
/// Serialize additional properties for Open Api v3.1.
/// </summary>
public void SerializeAsV3(IOpenApiWriter writer)
/// <param name="writer"></param>
protected virtual void SerializeAdditionalV31Properties(IOpenApiWriter writer)
{
// noop for the base type
}

/// <inheritdoc/>
public virtual void SerializeAsV3(IOpenApiWriter writer)
{
SerializeInternal(writer);
}

/// <summary>
/// Serialize <see cref="OpenApiReference"/>
/// Serialize <see cref="BaseOpenApiReference"/>
/// </summary>
private void SerializeInternal(IOpenApiWriter writer, Action<IOpenApiWriter>? callback = null)
{
Expand All @@ -206,10 +191,8 @@
writer.WriteEndObject();
}

/// <summary>
/// Serialize <see cref="OpenApiReference"/> to Open Api v2.0.
/// </summary>
public void SerializeAsV2(IOpenApiWriter writer)
/// <inheritdoc/>
public virtual void SerializeAsV2(IOpenApiWriter writer)
{
Utils.CheckArgumentNull(writer);

Expand Down Expand Up @@ -291,23 +274,27 @@
Utils.CheckArgumentNull(currentDocument);
hostDocument ??= currentDocument;
}
private static string? GetPropertyValueFromNode(JsonObject jsonObject, string key) =>
/// <summary>
/// Gets the property value from a JsonObject node.
/// </summary>
/// <param name="jsonObject">The object to get the value from</param>
/// <param name="key">The key of the property</param>
/// <returns>The property value</returns>
protected internal static string? GetPropertyValueFromNode(JsonObject jsonObject, string key) =>
jsonObject.TryGetPropertyValue(key, out var valueNode) && valueNode is JsonValue valueCast && valueCast.TryGetValue<string>(out var strValue) ? strValue : null;
internal void SetSummaryAndDescriptionFromMapNode(MapNode mapNode)
internal virtual void SetMetadataFromMapNode(MapNode mapNode)
{
var (description, summary) = mapNode.JsonNode switch {
JsonObject jsonObject => (GetPropertyValueFromNode(jsonObject, OpenApiConstants.Description),
GetPropertyValueFromNode(jsonObject, OpenApiConstants.Summary)),
_ => (null, null)
};
if (!string.IsNullOrEmpty(description))
{
Description = description;
}
if (!string.IsNullOrEmpty(summary))
{
Summary = summary;
}
if (mapNode.JsonNode is not JsonObject jsonObject) return;
SetAdditional31MetadataFromMapNode(jsonObject);
}

/// <summary>
/// Sets additional metadata from the map node.
/// </summary>
/// <param name="jsonObject">The object to get the data from</param>
protected virtual void SetAdditional31MetadataFromMapNode(JsonObject jsonObject)
{
// noop for the base type
}

internal void SetJsonPointerPath(string pointer, string nodeLocation)
Expand All @@ -319,11 +306,11 @@
}

// Absolute reference or anchor (e.g. "#/components/schemas/..." or full URL)
else if ((pointer.Contains('#') || pointer.StartsWith("http", StringComparison.OrdinalIgnoreCase))
else if ((pointer.Contains('#') || pointer.StartsWith("http", StringComparison.OrdinalIgnoreCase))
&& !string.Equals(ReferenceV3, pointer, StringComparison.OrdinalIgnoreCase))
{
ReferenceV3 = pointer;
}
}
}

private static string ResolveRelativePointer(string nodeLocation, string relativeRef)
Expand Down
139 changes: 139 additions & 0 deletions src/Microsoft.OpenApi/Models/JsonSchemaReference.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;

namespace Microsoft.OpenApi;

/// <summary>
/// Schema reference information that includes metadata annotations from JSON Schema 2020-12.
/// This class extends OpenApiReference to provide schema-specific metadata override capabilities.
/// </summary>
public class JsonSchemaReference : OpenApiReferenceWithDescription
{
/// <summary>
/// A default value which by default SHOULD override that of the referenced component.
/// If the referenced object-type does not allow a default field, then this field has no effect.
/// </summary>
public JsonNode? Default { get; set; }

/// <summary>
/// A title which by default SHOULD override that of the referenced component.
/// If the referenced object-type does not allow a title field, then this field has no effect.
/// </summary>
public string? Title { get; set; }

/// <summary>
/// Indicates whether the referenced component is deprecated.
/// If the referenced object-type does not allow a deprecated field, then this field has no effect.
/// </summary>
public bool? Deprecated { get; set; }

/// <summary>
/// Indicates whether the referenced component is read-only.
/// If the referenced object-type does not allow a readOnly field, then this field has no effect.
/// </summary>
public bool? ReadOnly { get; set; }

/// <summary>
/// Indicates whether the referenced component is write-only.
/// If the referenced object-type does not allow a writeOnly field, then this field has no effect.
/// </summary>
public bool? WriteOnly { get; set; }

/// <summary>
/// Example values which by default SHOULD override those of the referenced component.
/// If the referenced object-type does not allow examples, then this field has no effect.
/// </summary>
public IList<JsonNode>? Examples { get; set; }

/// <summary>
/// Parameterless constructor
/// </summary>
public JsonSchemaReference() { }

/// <summary>
/// Initializes a copy instance of the <see cref="JsonSchemaReference"/> object
/// </summary>
public JsonSchemaReference(JsonSchemaReference reference) : base(reference)
{
Utils.CheckArgumentNull(reference);
Default = reference.Default;
Title = reference.Title;
Deprecated = reference.Deprecated;
ReadOnly = reference.ReadOnly;
WriteOnly = reference.WriteOnly;
Examples = reference.Examples;
}

/// <inheritdoc/>
protected override void SerializeAdditionalV31Properties(IOpenApiWriter writer)
{
if (Type != ReferenceType.Schema) throw new InvalidOperationException(
$"JsonSchemaReference can only be serialized for ReferenceType.Schema, but was {Type}.");

base.SerializeAdditionalV31Properties(writer);
// Additional schema metadata annotations in 3.1
writer.WriteOptionalObject(OpenApiConstants.Default, Default, (w, d) => w.WriteAny(d));
writer.WriteProperty(OpenApiConstants.Title, Title);
if (Deprecated.HasValue)
{
writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated.Value, false);
}
if (ReadOnly.HasValue)
{
writer.WriteProperty(OpenApiConstants.ReadOnly, ReadOnly.Value, false);
}
if (WriteOnly.HasValue)
{
writer.WriteProperty(OpenApiConstants.WriteOnly, WriteOnly.Value, false);
}
if (Examples != null && Examples.Any())
{
writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (w, e) => w.WriteAny(e));
}
}

/// <inheritdoc/>
protected override void SetAdditional31MetadataFromMapNode(JsonObject jsonObject)
{
base.SetAdditional31MetadataFromMapNode(jsonObject);

var title = GetPropertyValueFromNode(jsonObject, OpenApiConstants.Title);
if (!string.IsNullOrEmpty(title))
{
Title = title;
}

// Boolean properties
if (jsonObject.TryGetPropertyValue(OpenApiConstants.Deprecated, out var deprecatedNode) && deprecatedNode is JsonValue deprecatedValue && deprecatedValue.TryGetValue<bool>(out var deprecated))
{
Deprecated = deprecated;
}

if (jsonObject.TryGetPropertyValue(OpenApiConstants.ReadOnly, out var readOnlyNode) && readOnlyNode is JsonValue readOnlyValue && readOnlyValue.TryGetValue<bool>(out var readOnly))
{
ReadOnly = readOnly;
}

if (jsonObject.TryGetPropertyValue(OpenApiConstants.WriteOnly, out var writeOnlyNode) && writeOnlyNode is JsonValue writeOnlyValue && writeOnlyValue.TryGetValue<bool>(out var writeOnly))
{
WriteOnly = writeOnly;
}

// Default value
if (jsonObject.TryGetPropertyValue(OpenApiConstants.Default, out var defaultNode))
{
Default = defaultNode;
}

// Examples
if (jsonObject.TryGetPropertyValue(OpenApiConstants.Examples, out var examplesNode) && examplesNode is JsonArray examplesArray)
{
Examples = examplesArray.OfType<JsonNode>().ToList();
}
}
}
8 changes: 4 additions & 4 deletions src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -496,9 +496,9 @@ public void SetReferenceHostDocument()
}

/// <summary>
/// Load the referenced <see cref="IOpenApiReferenceable"/> object from a <see cref="OpenApiReference"/> object
/// Load the referenced <see cref="IOpenApiReferenceable"/> object from a <see cref="BaseOpenApiReference"/> object
/// </summary>
internal T? ResolveReferenceTo<T>(OpenApiReference reference) where T : IOpenApiReferenceable
internal T? ResolveReferenceTo<T>(BaseOpenApiReference reference) where T : IOpenApiReferenceable
{

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

/// <summary>
/// Load the referenced <see cref="IOpenApiReferenceable"/> object from a <see cref="OpenApiReference"/> object
/// Load the referenced <see cref="IOpenApiReferenceable"/> object from a <see cref="BaseOpenApiReference"/> object
/// </summary>
internal IOpenApiReferenceable? ResolveReference(OpenApiReference? reference, bool useExternal)
internal IOpenApiReferenceable? ResolveReference(BaseOpenApiReference? reference, bool useExternal)
{
if (reference == null)
{
Expand Down
Loading