Skip to content
127 changes: 124 additions & 3 deletions src/Microsoft.OpenApi/Models/JsonSchemaReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,47 @@ public class JsonSchemaReference : OpenApiReferenceWithDescription
/// </summary>
public IDictionary<string, IOpenApiExtension>? Extensions { get; set; }

/// <summary>
/// A $id which by default SHOULD override that of the referenced component.
/// Named SchemaId to avoid collision with the inherited reference identifier (BaseOpenApiReference.Id).
/// </summary>
public string? SchemaId { get; set; }

/// <summary>
/// The $schema dialect URI which by default SHOULD override that of the referenced component.
/// </summary>
public Uri? Schema { get; set; }

/// <summary>
/// A $comment which by default SHOULD override that of the referenced component.
/// </summary>
public string? Comment { get; set; }

/// <summary>
/// The $vocabulary which by default SHOULD override that of the referenced component.
/// </summary>
public IDictionary<string, bool>? Vocabulary { get; set; }

/// <summary>
/// The $dynamicRef which by default SHOULD override that of the referenced component.
/// </summary>
public string? DynamicRef { get; set; }

/// <summary>
/// The $dynamicAnchor which by default SHOULD override that of the referenced component.
/// </summary>
public string? DynamicAnchor { get; set; }

/// <summary>
/// The $defs which by default SHOULD override that of the referenced component.
/// </summary>
public IDictionary<string, IOpenApiSchema>? Definitions { get; set; }

/// <summary>
/// The $anchor which by default SHOULD override that of the referenced component.
/// </summary>
public string? Anchor { get; set; }

/// <summary>
/// Parameterless constructor
/// </summary>
Expand All @@ -76,24 +117,50 @@ public JsonSchemaReference(JsonSchemaReference reference) : base(reference)
WriteOnly = reference.WriteOnly;
Examples = reference.Examples;
Extensions = reference.Extensions != null ? new Dictionary<string, IOpenApiExtension>(reference.Extensions) : null;
SchemaId = reference.SchemaId;
Schema = reference.Schema;
Comment = reference.Comment;
Vocabulary = reference.Vocabulary != null ? new Dictionary<string, bool>(reference.Vocabulary) : null;
DynamicRef = reference.DynamicRef;
DynamicAnchor = reference.DynamicAnchor;
Definitions = reference.Definitions != null ? new Dictionary<string, IOpenApiSchema>(reference.Definitions) : null;
Anchor = reference.Anchor;
}

/// <inheritdoc/>
protected override void SerializeAdditionalV31Properties(IOpenApiWriter writer)
{
SerializeAdditionalV3XProperties(writer, base.SerializeAdditionalV31Properties);
SerializeAdditionalV3XProperties(writer, OpenApiSpecVersion.OpenApi3_1, base.SerializeAdditionalV31Properties);
}
/// <inheritdoc/>
protected override void SerializeAdditionalV32Properties(IOpenApiWriter writer)
{
SerializeAdditionalV3XProperties(writer, base.SerializeAdditionalV32Properties);
SerializeAdditionalV3XProperties(writer, OpenApiSpecVersion.OpenApi3_2, base.SerializeAdditionalV32Properties);
}
private void SerializeAdditionalV3XProperties(IOpenApiWriter writer, Action<IOpenApiWriter> baseSerializer)
private void SerializeAdditionalV3XProperties(IOpenApiWriter writer, OpenApiSpecVersion version, Action<IOpenApiWriter> baseSerializer)
{
if (Type != ReferenceType.Schema) throw new InvalidOperationException(
$"JsonSchemaReference can only be serialized for ReferenceType.Schema, but was {Type}.");

baseSerializer(writer);

// JSON Schema 2020-12 keyword siblings (preserved per OAS 3.1+ / JSON Schema 2020-12 semantics)
writer.WriteProperty(OpenApiConstants.Id, SchemaId);
writer.WriteProperty(OpenApiConstants.DollarSchema, Schema?.ToString());
writer.WriteProperty(OpenApiConstants.Comment, Comment);
writer.WriteOptionalMap(OpenApiConstants.Vocabulary, Vocabulary, (w, s) => w.WriteValue(s));
if (version == OpenApiSpecVersion.OpenApi3_1)
{
writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, (w, s) => s.SerializeAsV31(w));
}
else
{
writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, (w, s) => s.SerializeAsV32(w));
}
writer.WriteProperty(OpenApiConstants.Anchor, Anchor);
writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef);
writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor);

// Additional schema metadata annotations in 3.1
writer.WriteOptionalObject(OpenApiConstants.Default, Default, (w, d) => w.WriteAny(d));
writer.WriteProperty(OpenApiConstants.Title, Title);
Expand Down Expand Up @@ -164,5 +231,59 @@ protected override void SetAdditional31MetadataFromMapNode(JsonObject jsonObject
Extensions ??= new Dictionary<string, IOpenApiExtension>(StringComparer.OrdinalIgnoreCase);
Extensions[property.Key] = new JsonNodeExtension(extensionValue.DeepClone());
}

// JSON Schema 2020-12 keyword siblings ($defs is parsed separately in the deserializer
// because it requires LoadSchema for nested schema materialization)
var id = GetPropertyValueFromNode(jsonObject, OpenApiConstants.Id);
if (!string.IsNullOrEmpty(id))
{
SchemaId = id;
}

var schemaValue = GetPropertyValueFromNode(jsonObject, OpenApiConstants.DollarSchema);
if (!string.IsNullOrEmpty(schemaValue) && Uri.TryCreate(schemaValue, UriKind.Absolute, out var schemaUri))
{
Schema = schemaUri;
}

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

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

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

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

if (jsonObject.TryGetPropertyValue(OpenApiConstants.Vocabulary, out var vocabNode) && vocabNode is JsonObject vocabObj)
{
var vocab = new Dictionary<string, bool>();
foreach (var kvp in vocabObj)
{
if (kvp.Value is JsonValue v && v.TryGetValue<bool>(out var b))
{
vocab[kvp.Key] = b;
}
}
if (vocab.Count > 0)
{
Vocabulary = vocab;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,21 @@ public string? Title
set => Reference.Title = value;
}
/// <inheritdoc/>
public Uri? Schema { get => Target?.Schema; }
public Uri? Schema { get => Reference.Schema ?? Target?.Schema; }
/// <inheritdoc/>
public string? Id { get => Target?.Id; }
public string? Id { get => string.IsNullOrEmpty(Reference.SchemaId) ? Target?.Id : Reference.SchemaId; }
/// <inheritdoc/>
public string? Comment { get => Target?.Comment; }
public string? Comment { get => string.IsNullOrEmpty(Reference.Comment) ? Target?.Comment : Reference.Comment; }
/// <inheritdoc/>
public IDictionary<string, bool>? Vocabulary { get => Target?.Vocabulary; }
public IDictionary<string, bool>? Vocabulary { get => Reference.Vocabulary ?? Target?.Vocabulary; }
/// <inheritdoc/>
public string? DynamicRef { get => Target?.DynamicRef; }
public string? DynamicRef { get => string.IsNullOrEmpty(Reference.DynamicRef) ? Target?.DynamicRef : Reference.DynamicRef; }
/// <inheritdoc/>
public string? DynamicAnchor { get => Target?.DynamicAnchor; }
public string? DynamicAnchor { get => string.IsNullOrEmpty(Reference.DynamicAnchor) ? Target?.DynamicAnchor : Reference.DynamicAnchor; }
/// <inheritdoc/>
public IDictionary<string, IOpenApiSchema>? Definitions { get => Target?.Definitions; }
public IDictionary<string, IOpenApiSchema>? Definitions { get => Reference.Definitions ?? Target?.Definitions; }
/// <inheritdoc/>
public string? Anchor { get => (Target as IOpenApiSchemaMissingProperties)?.Anchor; }
public string? Anchor { get => string.IsNullOrEmpty(Reference.Anchor) ? (Target as IOpenApiSchemaMissingProperties)?.Anchor : Reference.Anchor; }
/// <inheritdoc/>
public string? ExclusiveMaximum { get => Target?.ExclusiveMaximum; }
/// <inheritdoc/>
Expand Down
16 changes: 16 additions & 0 deletions src/Microsoft.OpenApi/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
#nullable enable
Microsoft.OpenApi.JsonSchemaReference.Anchor.get -> string?
Microsoft.OpenApi.JsonSchemaReference.Anchor.set -> void
Microsoft.OpenApi.JsonSchemaReference.Comment.get -> string?
Microsoft.OpenApi.JsonSchemaReference.Comment.set -> void
Microsoft.OpenApi.JsonSchemaReference.Definitions.get -> System.Collections.Generic.IDictionary<string!, Microsoft.OpenApi.IOpenApiSchema!>?
Microsoft.OpenApi.JsonSchemaReference.Definitions.set -> void
Microsoft.OpenApi.JsonSchemaReference.DynamicAnchor.get -> string?
Microsoft.OpenApi.JsonSchemaReference.DynamicAnchor.set -> void
Microsoft.OpenApi.JsonSchemaReference.DynamicRef.get -> string?
Microsoft.OpenApi.JsonSchemaReference.DynamicRef.set -> void
Microsoft.OpenApi.JsonSchemaReference.Schema.get -> System.Uri?
Microsoft.OpenApi.JsonSchemaReference.Schema.set -> void
Microsoft.OpenApi.JsonSchemaReference.SchemaId.get -> string?
Microsoft.OpenApi.JsonSchemaReference.SchemaId.set -> void
Microsoft.OpenApi.JsonSchemaReference.Vocabulary.get -> System.Collections.Generic.IDictionary<string!, bool>?
Microsoft.OpenApi.JsonSchemaReference.Vocabulary.set -> void
25 changes: 25 additions & 0 deletions src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,31 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
var result = new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2);
result.Reference.SetMetadataFromJsonObject(jsonObject);
result.Reference.SetJsonPointerPath(pointer, nodeLocation);

// Parse $defs sibling — requires LoadSchema for nested schema materialization,
// so it cannot be done inside SetAdditional31MetadataFromMapNode.
if (jsonObject.TryGetPropertyValue(OpenApiConstants.Defs, out var defsNode) && defsNode is JsonObject defsObj)
{
var defs = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
foreach (var kvp in defsObj)
{
if (kvp.Value is null) continue;
context.StartObject(kvp.Key);
try
{
defs[kvp.Key] = LoadSchema(kvp.Value, hostDocument, context);
}
finally
{
context.EndObject();
}
}
if (defs.Count > 0)
{
result.Reference.Definitions = defs;
}
}

return result;
}

Expand Down
25 changes: 25 additions & 0 deletions src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,31 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
var result = new OpenApiSchemaReference(reference.Item1, hostDocument, reference.Item2);
result.Reference.SetMetadataFromJsonObject(jsonObject);
result.Reference.SetJsonPointerPath(pointer, nodeLocation);

// Parse $defs sibling — requires LoadSchema for nested schema materialization,
// so it cannot be done inside SetAdditional31MetadataFromMapNode.
if (jsonObject.TryGetPropertyValue(OpenApiConstants.Defs, out var defsNode) && defsNode is JsonObject defsObj)
{
var defs = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
foreach (var kvp in defsObj)
{
if (kvp.Value is null) continue;
context.StartObject(kvp.Key);
try
{
defs[kvp.Key] = LoadSchema(kvp.Value, hostDocument, context);
}
finally
{
context.EndObject();
}
}
if (defs.Count > 0)
{
result.Reference.Definitions = defs;
}
}

return result;
}

Expand Down
Loading