Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ private static string ConvertByteArrayToString(byte[] hash)
// Enables setting the complete JSON path for nested subschemas e.g. #/components/schemas/person/properties/address
if (useExternal)
{
var relPathSegment = referenceV3.Split(['#'], StringSplitOptions.RemoveEmptyEntries)[1];
var relPathSegment = referenceV3.Split(new char[] {'#'}, StringSplitOptions.RemoveEmptyEntries)[1];
relativePath = $"#{relPathSegment}";
}
else
Expand Down Expand Up @@ -625,7 +625,7 @@ private static bool IsSubComponent(string reference)

if (fragment.StartsWith("/components/schemas/", StringComparison.OrdinalIgnoreCase))
{
var segments = fragment.Split(['/'], StringSplitOptions.RemoveEmptyEntries);
var segments = fragment.Split(new char[] {'/'}, StringSplitOptions.RemoveEmptyEntries);

// Expect exactly 3 segments for root-level schema: ["components", "schemas", "person"]
// Anything longer means it's a subcomponent.
Expand Down
20 changes: 12 additions & 8 deletions src/Microsoft.OpenApi/Models/OpenApiReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Reader;
Expand All @@ -26,6 +27,8 @@ public class OpenApiReference : IOpenApiSerializable, IOpenApiDescribedElement,
/// </summary>
public string? Description { get; set; }



/// <summary>
/// External resource in the reference.
/// It maybe:
Expand Down Expand Up @@ -293,13 +296,14 @@ internal void EnsureHostDocumentIsSet(OpenApiDocument currentDocument)
}
private 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 void SetMetadataFromMapNode(MapNode mapNode)
{
var (description, summary) = mapNode.JsonNode switch {
JsonObject jsonObject => (GetPropertyValueFromNode(jsonObject, OpenApiConstants.Description),
GetPropertyValueFromNode(jsonObject, OpenApiConstants.Summary)),
_ => (null, null)
};
if (mapNode.JsonNode is not JsonObject jsonObject) return;

// Summary and Description
var description = GetPropertyValueFromNode(jsonObject, OpenApiConstants.Description);
var summary = GetPropertyValueFromNode(jsonObject, OpenApiConstants.Summary);

if (!string.IsNullOrEmpty(description))
{
Description = description;
Expand Down Expand Up @@ -329,10 +333,10 @@ internal void SetJsonPointerPath(string pointer, string nodeLocation)
private static string ResolveRelativePointer(string nodeLocation, string relativeRef)
{
// Convert nodeLocation to path segments
var segments = nodeLocation.TrimStart('#').Split(['/'], StringSplitOptions.RemoveEmptyEntries).ToList();
var segments = nodeLocation.TrimStart('#').Split(new char[] {'/'}, StringSplitOptions.RemoveEmptyEntries).ToList();

// Convert relativeRef to dynamic segments
var relativeSegments = relativeRef.TrimStart('#').Split(['/'], StringSplitOptions.RemoveEmptyEntries);
var relativeSegments = relativeRef.TrimStart('#').Split(new char[] {'/'}, StringSplitOptions.RemoveEmptyEntries);

// Locate the first occurrence of relativeRef segments in the full path
for (int i = 0; i <= segments.Count - relativeSegments.Length; i++)
Expand Down
181 changes: 181 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiSchemaReferenceInformation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Reader;

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 OpenApiSchemaReferenceInformation : OpenApiReference
{
/// <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 OpenApiSchemaReferenceInformation() { }

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

/// <summary>
/// Serialize <see cref="OpenApiSchemaReferenceInformation"/> to Open Api v3.1.
/// </summary>
public new void SerializeAsV31(IOpenApiWriter writer)
{
Utils.CheckArgumentNull(writer);

if (Type == ReferenceType.Tag && !string.IsNullOrEmpty(ReferenceV3) && ReferenceV3 is not null)
{
// Write the string value only
writer.WriteValue(ReferenceV3);
return;
}

writer.WriteStartObject();

// summary and description are in 3.1 but not in 3.0
writer.WriteProperty(OpenApiConstants.Summary, Summary);
writer.WriteProperty(OpenApiConstants.Description, Description);

// 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));
}

// $ref
writer.WriteProperty(OpenApiConstants.DollarRef, ReferenceV3);

writer.WriteEndObject();
}

/// <summary>
/// Sets metadata fields from a JSON node during parsing
/// </summary>
internal new void SetMetadataFromMapNode(MapNode mapNode)
{
base.SetMetadataFromMapNode(mapNode);

if (mapNode.JsonNode is not JsonObject jsonObject) return;

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)
{
if (deprecatedValue.TryGetValue<bool>(out var deprecated))
{
Deprecated = deprecated;
}
}

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

if (jsonObject.TryGetPropertyValue(OpenApiConstants.WriteOnly, out var writeOnlyNode) && writeOnlyNode is JsonValue writeOnlyValue)
{
if (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 = new List<JsonNode>();
foreach (var example in examplesArray)
{
if (example != null)
{
Examples.Add(example);
}
}
}
}

private 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;
}
}
Loading
Loading