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));
}
Comment on lines +152 to +159

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need to use the version instead of the callback?
this is brittle and we might forget to update things here in future versions

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);
Comment on lines 164 to 166
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>?
Comment on lines +2 to +16

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In JSON schema 2020-12 only annotation keywords are allowed next to $ref.
$anchor, $dynamicRef, $dynamicAnchor, $defs, $schema $id and $vocabulary are all core vocabulary keywords.
The only exception is $comment that's technically core vocabulary but allowed anyway.
The only fields that implement this behaviour (target vs reference value) are the annotation keywords (default, readOnly....)

Let me know if you have any additional comments or questions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, after digging some more, it seems that:

I couldn't find any additional evidence for $anchor, $dynamicAnchor or $vocabulary

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually I went ahead and had additional discussions with both @handrews and @darrelmiller (thank you both). My understanding was wrong.
Any JSON Schema keyword can appear next to $ref, some might not be applicable (not do anything) due to their nature, some might be contradictory when appearing both in the reference and the referenced schema, in which case, it's up to the application to define what the behaviour should be.

In the case where both are present, we have a precedent of returning the reference value for annotation keywords, so being consistent here makes sense. Then the application can check the reference and the target values, and compare them, if the difference is important to the application.

Any keyword in JSON schema exposed as a property in the IOpenAPISchema interface as well as in the IOpenApiSchemaMissingProperties should have:

  • a corresponding setter in the JsonSchemaReference type, and serialization code to match (use x-jsonschema prefix for OAI < 3.1)
  • a corresponding getter in the JsonSchemaReference type, and the deserialization code to match (use x-jsonschema prefix for OAI < 3.1)
  • a corresponding setter in OpenApiSchemaReference, that maps to the reference corresponding setter
  • a corresponding getter, which returns the reference value when present, otherwise the target value
  • cloning and unit test code like you've started

Now, I understand this is significant work, if you want to focus on the properties you've identified first, and punt the other properties to an additional issue, let me know.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could refactor that to accept a callback. Assuming the SetMetadata method is internal

{
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;
}
}
Comment on lines +456 to +478

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;
}
}
Comment on lines +456 to +478

return result;
}

Expand Down
Loading