diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs
index 51187591c..214b13419 100644
--- a/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs
+++ b/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs
@@ -79,9 +79,21 @@ public string? Title
///
public string? Minimum { get => string.IsNullOrEmpty(Reference.Minimum) ? Target?.Minimum : Reference.Minimum; set => Reference.Minimum = value; }
///
- public int? MaxLength { get => Reference.MaxLength ?? Target?.MaxLength; set => Reference.MaxLength = value; }
+ public int? MaxLength
+ {
+ get => Target?.MaxLength is { } tMax && Reference.MaxLength is { } rMax
+ ? Math.Min(tMax, rMax)
+ : Target?.MaxLength ?? Reference.MaxLength;
+ set => Reference.MaxLength = value;
+ }
///
- public int? MinLength { get => Reference.MinLength ?? Target?.MinLength; set => Reference.MinLength = value; }
+ public int? MinLength
+ {
+ get => Target?.MinLength is { } tMin && Reference.MinLength is { } rMin
+ ? Math.Max(tMin, rMin)
+ : Target?.MinLength ?? Reference.MinLength;
+ set => Reference.MinLength = value;
+ }
///
public string? Pattern { get => string.IsNullOrEmpty(Reference.Pattern) ? Target?.Pattern : Reference.Pattern; set => Reference.Pattern = value; }
///
@@ -95,13 +107,13 @@ public JsonNode? Default
///
public bool ReadOnly
{
- get => Reference.ReadOnly ?? Target?.ReadOnly ?? false;
+ get => Reference.ReadOnly == true || Target?.ReadOnly == true;
set => Reference.ReadOnly = value;
}
///
public bool WriteOnly
{
- get => Reference.WriteOnly ?? Target?.WriteOnly ?? false;
+ get => Reference.WriteOnly == true || Target?.WriteOnly == true;
set => Reference.WriteOnly = value;
}
///
@@ -117,9 +129,21 @@ public bool WriteOnly
///
public IOpenApiSchema? Items { get => Reference.Items ?? Target?.Items; set => Reference.Items = value; }
///
- public int? MaxItems { get => Reference.MaxItems ?? Target?.MaxItems; set => Reference.MaxItems = value; }
+ public int? MaxItems
+ {
+ get => Target?.MaxItems is { } tMax && Reference.MaxItems is { } rMax
+ ? Math.Min(tMax, rMax)
+ : Target?.MaxItems ?? Reference.MaxItems;
+ set => Reference.MaxItems = value;
+ }
///
- public int? MinItems { get => Reference.MinItems ?? Target?.MinItems; set => Reference.MinItems = value; }
+ public int? MinItems
+ {
+ get => Target?.MinItems is { } tMin && Reference.MinItems is { } rMin
+ ? Math.Max(tMin, rMin)
+ : Target?.MinItems ?? Reference.MinItems;
+ set => Reference.MinItems = value;
+ }
///
public bool? UniqueItems { get => Reference.UniqueItems ?? Target?.UniqueItems; set => Reference.UniqueItems = value; }
///
@@ -133,11 +157,27 @@ public bool WriteOnly
///
public IDictionary? PatternProperties { get => Reference.PatternProperties ?? Target?.PatternProperties; set => Reference.PatternProperties = value; }
///
- public int? MaxProperties { get => Reference.MaxProperties ?? Target?.MaxProperties; set => Reference.MaxProperties = value; }
+ public int? MaxProperties
+ {
+ get => Target?.MaxProperties is { } tMax && Reference.MaxProperties is { } rMax
+ ? Math.Min(tMax, rMax)
+ : Target?.MaxProperties ?? Reference.MaxProperties;
+ set => Reference.MaxProperties = value;
+ }
///
- public int? MinProperties { get => Reference.MinProperties ?? Target?.MinProperties; set => Reference.MinProperties = value; }
+ public int? MinProperties
+ {
+ get => Target?.MinProperties is { } tMin && Reference.MinProperties is { } rMin
+ ? Math.Max(tMin, rMin)
+ : Target?.MinProperties ?? Reference.MinProperties;
+ set => Reference.MinProperties = value;
+ }
///
- public bool AdditionalPropertiesAllowed { get => Reference.AdditionalPropertiesAllowed ?? Target?.AdditionalPropertiesAllowed ?? true; set => Reference.AdditionalPropertiesAllowed = value; }
+ public bool AdditionalPropertiesAllowed
+ {
+ get => Reference.AdditionalPropertiesAllowed ?? Target?.AdditionalPropertiesAllowed ?? true;
+ set => Reference.AdditionalPropertiesAllowed = value;
+ }
///
public IOpenApiSchema? AdditionalProperties { get => Reference.AdditionalProperties ?? Target?.AdditionalProperties; set => Reference.AdditionalProperties = value; }
///
@@ -153,7 +193,11 @@ public IList? Examples
///
public IList? Enum { get => Reference.Enum ?? Target?.Enum; set => Reference.Enum = value; }
///
- public bool UnevaluatedProperties { get => Reference.UnevaluatedProperties ?? Target?.UnevaluatedProperties ?? true; set => Reference.UnevaluatedProperties = value; }
+ public bool UnevaluatedProperties
+ {
+ get => Reference.UnevaluatedProperties ?? Target?.UnevaluatedProperties ?? true;
+ set => Reference.UnevaluatedProperties = value;
+ }
///
public IOpenApiSchema? UnevaluatedPropertiesSchema { get => Reference.UnevaluatedPropertiesSchema ?? (Target as IOpenApiSchemaMissingProperties)?.UnevaluatedPropertiesSchema; set => Reference.UnevaluatedPropertiesSchema = value; }
///
diff --git a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs
index 8f6cb3107..d988c9121 100644
--- a/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs
+++ b/test/Microsoft.OpenApi.Tests/Models/References/OpenApiSchemaReferenceTests.cs
@@ -165,7 +165,7 @@ public void SchemaReferenceExposesMissingPropertiesFromTarget()
}
[Fact]
- public void SchemaReferenceWithKeywordSiblingsShouldOverrideTargetValues()
+ public void SchemaReferenceWithKeywordSiblingsDoesNotOverrideTargetValues()
{
var workingDocument = new OpenApiDocument
{
@@ -206,16 +206,33 @@ public void SchemaReferenceWithKeywordSiblingsShouldOverrideTargetValues()
If = new OpenApiSchema { Required = new HashSet { "reference" } }
};
+ // Non-lossless keywords remain reference-first convenience accessors.
Assert.Equal(JsonSchemaType.String, schemaReference.Type);
Assert.Equal("reference-format", schemaReference.Format);
- Assert.Equal(10, schemaReference.MaxLength);
+ Assert.Equal(JsonSchemaType.Integer, schemaReference.Properties?["reference"].Type);
+ Assert.NotNull(schemaReference.If?.Required);
+ Assert.Contains("reference", schemaReference.If.Required);
+
+ // Numeric bounds: stricter value wins (both constraints apply).
+ Assert.Equal(10, schemaReference.MaxLength); // Math.Min(20, 10)
+
+ // Required is a mutable sibling collection, not an effective merged set.
Assert.NotNull(schemaReference.Required);
Assert.Contains("reference", schemaReference.Required);
- Assert.Equal(JsonSchemaType.Integer, schemaReference.Properties?["reference"].Type);
+ Assert.DoesNotContain("target", schemaReference.Required);
+
+ // AdditionalPropertiesAllowed is reference-first (location-sensitive, not collapsible).
Assert.False(schemaReference.AdditionalPropertiesAllowed);
+
+ // ContentEncoding is an annotation — sibling value takes precedence.
Assert.Equal("base64", schemaReference.ContentEncoding);
- Assert.NotNull(schemaReference.If?.Required);
- Assert.Contains("reference", schemaReference.If.Required);
+
+ // Authored sibling values are still stored on the Reference.
+ Assert.Equal(JsonSchemaType.String, schemaReference.Reference.SchemaType);
+ Assert.Equal("reference-format", schemaReference.Reference.Format);
+ Assert.Equal(10, schemaReference.Reference.MaxLength);
+ Assert.Contains("reference", schemaReference.Reference.Required ?? new HashSet());
+ Assert.False(schemaReference.Reference.AdditionalPropertiesAllowed ?? true);
}
[Fact]
@@ -276,16 +293,37 @@ public void ParseSchemaReferenceWithKeywordSiblingsWorks()
Assert.Empty(readResult.Diagnostic.Errors);
var schemaReference = Assert.IsType(readResult.Document?.Paths["/test"].Operations[HttpMethod.Get]
.Responses["200"].Content["application/json"].Schema);
+
+ // Type/Format: non-lossless convenience getters remain reference-first.
Assert.Equal(JsonSchemaType.String, schemaReference.Type);
Assert.Equal("uuid", schemaReference.Format);
- Assert.Equal(36, schemaReference.MaxLength);
+
+ // MaxLength: stricter wins — Math.Min(10, 36) = 10.
+ Assert.Equal(10, schemaReference.MaxLength);
+
+ // Required: target has none, sibling has ["id"] → union = ["id"].
Assert.NotNull(schemaReference.Required);
Assert.Contains("id", schemaReference.Required);
+
+ // Properties: target has none → sibling fallback.
Assert.Equal(JsonSchemaType.String, schemaReference.Properties?["id"].Type);
+
+ // AdditionalPropertiesAllowed: target true, sibling false → false.
Assert.False(schemaReference.AdditionalPropertiesAllowed);
- Assert.Equal("base64", schemaReference.ContentEncoding);
+
+ // If: target has none → sibling fallback.
Assert.NotNull(schemaReference.If?.Required);
Assert.Contains("id", schemaReference.If.Required);
+
+ // ContentEncoding is an annotation — sibling value takes precedence.
+ Assert.Equal("base64", schemaReference.ContentEncoding);
+
+ // Authored sibling structural values are preserved on the Reference.
+ Assert.Equal(JsonSchemaType.String, schemaReference.Reference.SchemaType);
+ Assert.Equal("uuid", schemaReference.Reference.Format);
+ Assert.Equal(36, schemaReference.Reference.MaxLength);
+ Assert.Contains("id", schemaReference.Reference.Required ?? new HashSet());
+ Assert.False(schemaReference.Reference.AdditionalPropertiesAllowed ?? true);
}
[Theory]
@@ -612,5 +650,429 @@ public async Task SchemaReferenceExtensionsNotWrittenInV2()
// Assert: In v2, ONLY $ref should appear - no description, no extensions
Assert.Equal(@"{""$ref"":""#/definitions/Pet""}", output);
}
+
+ [Fact]
+ public void StructuralSiblingDoesNotRelaxTargetAssertion()
+ {
+ // Per JSON Schema 2020-12 (and handrews' comment on #2919):
+ // assertion keywords alongside $ref MUST be evaluated independently.
+ // A sibling minProperties: 2 does NOT relax a target minProperties: 5.
+ var workingDocument = new OpenApiDocument
+ {
+ Components = new OpenApiComponents(),
+ };
+ workingDocument.Components.Schemas = new Dictionary
+ {
+ ["Target"] = new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ MinProperties = 5
+ }
+ };
+ workingDocument.Workspace.RegisterComponents(workingDocument);
+
+ var schemaRef = new OpenApiSchemaReference("Target", workingDocument)
+ {
+ MinProperties = 2
+ };
+
+ // Math.Max(5, 2) = 5 — the stricter constraint applies.
+ Assert.Equal(5, schemaRef.MinProperties);
+ // Sibling value is preserved on the Reference.
+ Assert.Equal(2, schemaRef.Reference.MinProperties);
+ }
+
+ [Fact]
+ public void PropertiesSiblingRemainsVisibleOnConvenienceGetter()
+ {
+ // Per handrews: applicators like "properties" MUST be evaluated independently.
+ // A sibling properties keyword does not replace the target's properties.
+ var workingDocument = new OpenApiDocument
+ {
+ Components = new OpenApiComponents(),
+ };
+ workingDocument.Components.Schemas = new Dictionary
+ {
+ ["Target"] = new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ Properties = new Dictionary
+ {
+ ["name"] = new OpenApiSchema { Type = JsonSchemaType.String }
+ }
+ }
+ };
+ workingDocument.Workspace.RegisterComponents(workingDocument);
+
+ var schemaRef = new OpenApiSchemaReference("Target", workingDocument)
+ {
+ Properties = new Dictionary
+ {
+ ["id"] = new OpenApiSchema { Type = JsonSchemaType.Integer }
+ }
+ };
+
+ // Properties cannot be losslessly collapsed when both sides define the same property.
+ // Keep the convenience getter reference-first so the sibling applicator remains visible.
+ Assert.Contains("id", schemaRef.Properties?.Keys ?? new Dictionary().Keys);
+ Assert.DoesNotContain("name", schemaRef.Properties?.Keys ?? new Dictionary().Keys);
+ // Target value remains available separately.
+ Assert.Contains("name", schemaRef.Target?.Properties?.Keys ?? new Dictionary().Keys);
+ // Sibling value is preserved on the Reference.
+ Assert.Contains("id", schemaRef.Reference.Properties?.Keys ?? new Dictionary().Keys);
+ }
+
+ [Fact]
+ public void ContainsCountKeywordsStayAlignedWithSiblingContainsApplicator()
+ {
+ // contains/minContains/maxContains communicate as a group. Since contains
+ // cannot be losslessly collapsed, the related count keywords remain
+ // reference-first too.
+ var workingDocument = new OpenApiDocument
+ {
+ Components = new OpenApiComponents(),
+ };
+ workingDocument.Components.Schemas = new Dictionary
+ {
+ ["Target"] = new OpenApiSchema
+ {
+ Contains = new OpenApiSchema { Type = JsonSchemaType.String },
+ MinContains = 5,
+ MaxContains = 10
+ }
+ };
+ workingDocument.Workspace.RegisterComponents(workingDocument);
+
+ var schemaRef = new OpenApiSchemaReference("Target", workingDocument)
+ {
+ Contains = new OpenApiSchema { Type = JsonSchemaType.Number },
+ MinContains = 2,
+ MaxContains = 3
+ };
+
+ Assert.Equal(JsonSchemaType.Number, schemaRef.Contains?.Type);
+ Assert.Equal(2U, schemaRef.MinContains);
+ Assert.Equal(3U, schemaRef.MaxContains);
+ Assert.Equal(JsonSchemaType.String, schemaRef.Target is IOpenApiSchemaMissingProperties target ? target.Contains?.Type : null);
+ Assert.Equal(5U, schemaRef.Target is IOpenApiSchemaMissingProperties targetMin ? targetMin.MinContains : null);
+ Assert.Equal(10U, schemaRef.Target is IOpenApiSchemaMissingProperties targetMax ? targetMax.MaxContains : null);
+ }
+
+ [Fact]
+ public void ReadOnlyIsTrueIfEitherSourceIsTrue()
+ {
+ // Per handrews on #2919: if anything marks an instance location
+ // readOnly: true then it SHOULD be considered read-only even if
+ // some other subschema marks it readOnly: false.
+ var workingDocument = new OpenApiDocument
+ {
+ Components = new OpenApiComponents(),
+ };
+ workingDocument.Components.Schemas = new Dictionary
+ {
+ ["TargetRO"] = new OpenApiSchema { ReadOnly = true },
+ ["TargetNotRO"] = new OpenApiSchema { ReadOnly = false },
+ };
+ workingDocument.Workspace.RegisterComponents(workingDocument);
+
+ // Target readOnly=true, sibling not set → true
+ var ref1 = new OpenApiSchemaReference("TargetRO", workingDocument);
+ Assert.True(ref1.ReadOnly);
+
+ // Target readOnly=true, sibling readOnly=false → still true
+ var ref2 = new OpenApiSchemaReference("TargetRO", workingDocument) { ReadOnly = false };
+ Assert.True(ref2.ReadOnly);
+
+ // Target readOnly=false, sibling readOnly=true → true
+ var ref3 = new OpenApiSchemaReference("TargetNotRO", workingDocument) { ReadOnly = true };
+ Assert.True(ref3.ReadOnly);
+
+ // Target readOnly=false, sibling not set → false
+ var ref4 = new OpenApiSchemaReference("TargetNotRO", workingDocument);
+ Assert.False(ref4.ReadOnly);
+ }
+
+ [Fact]
+ public void WriteOnlyIsTrueIfEitherSourceIsTrue()
+ {
+ var workingDocument = new OpenApiDocument
+ {
+ Components = new OpenApiComponents(),
+ };
+ workingDocument.Components.Schemas = new Dictionary
+ {
+ ["TargetWO"] = new OpenApiSchema { WriteOnly = true },
+ ["TargetNotWO"] = new OpenApiSchema { WriteOnly = false },
+ };
+ workingDocument.Workspace.RegisterComponents(workingDocument);
+
+ var ref1 = new OpenApiSchemaReference("TargetWO", workingDocument);
+ Assert.True(ref1.WriteOnly);
+
+ var ref2 = new OpenApiSchemaReference("TargetWO", workingDocument) { WriteOnly = false };
+ Assert.True(ref2.WriteOnly);
+
+ var ref3 = new OpenApiSchemaReference("TargetNotWO", workingDocument) { WriteOnly = true };
+ Assert.True(ref3.WriteOnly);
+
+ var ref4 = new OpenApiSchemaReference("TargetNotWO", workingDocument);
+ Assert.False(ref4.WriteOnly);
+ }
+
+ [Fact]
+ public async Task ParseSchemaReferenceSiblingSerializationRoundTrip()
+ {
+ // Verifies that $ref siblings survive parse → serialize round-trip
+ // even though structural getters now resolve from Target.
+ var jsonContent = @"{
+ ""openapi"": ""3.1.0"",
+ ""info"": {
+ ""title"": ""Test API"",
+ ""version"": ""1.0.0""
+ },
+ ""paths"": {
+ ""/test"": {
+ ""get"": {
+ ""responses"": {
+ ""200"": {
+ ""description"": ""OK"",
+ ""content"": {
+ ""application/json"": {
+ ""schema"": {
+ ""$ref"": ""#/components/schemas/Target"",
+ ""minProperties"": 2,
+ ""description"": ""sibling description""
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ ""components"": {
+ ""schemas"": {
+ ""Target"": {
+ ""type"": ""object"",
+ ""minProperties"": 5
+ }
+ }
+ }
+}";
+
+ var readResult = OpenApiDocument.Parse(jsonContent, "json");
+ Assert.Empty(readResult.Diagnostic.Errors);
+
+ var doc = readResult.Document!;
+ var schemaRef = Assert.IsType(doc.Paths["/test"].Operations[HttpMethod.Get]
+ .Responses["200"].Content["application/json"].Schema);
+
+ // Structural getter resolves from Target.
+ Assert.Equal(5, schemaRef.MinProperties);
+ // Annotation getter resolves from sibling.
+ Assert.Equal("sibling description", schemaRef.Description);
+ // Sibling structural value preserved on Reference.
+ Assert.Equal(2, schemaRef.Reference.MinProperties);
+
+ // Serialize back and verify siblings round-trip on the $ref wire format.
+ using var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
+ var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = true });
+ schemaRef.SerializeAsV31(writer);
+ await writer.FlushAsync();
+ var output = outputStringWriter.ToString();
+
+ Assert.Contains("\"$ref\":\"#/components/schemas/Target\"", output);
+ Assert.Contains("\"minProperties\":2", output);
+ Assert.Contains("\"description\":\"sibling description\"", output);
+ }
+
+ [Fact]
+ public void StricterSiblingEnforcesTighterBound()
+ {
+ // A sibling minProperties: 7 is stricter than target minProperties: 5.
+ // Both apply, so the effective constraint is Math.Max(5, 7) = 7.
+ var workingDocument = new OpenApiDocument
+ {
+ Components = new OpenApiComponents(),
+ };
+ workingDocument.Components.Schemas = new Dictionary
+ {
+ ["Target"] = new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ MinProperties = 5,
+ MaxProperties = 20
+ }
+ };
+ workingDocument.Workspace.RegisterComponents(workingDocument);
+
+ var schemaRef = new OpenApiSchemaReference("Target", workingDocument)
+ {
+ MinProperties = 7,
+ MaxProperties = 10
+ };
+
+ Assert.Equal(7, schemaRef.MinProperties); // Math.Max(5, 7)
+ Assert.Equal(10, schemaRef.MaxProperties); // Math.Min(20, 10)
+ }
+
+ [Fact]
+ public void RawNumericBoundSiblingsRemainVisible()
+ {
+ // maximum/minimum are stored as raw strings to preserve JSON number precision.
+ // Do not parse and compare them here; keep sibling-authored values visible.
+ var workingDocument = new OpenApiDocument
+ {
+ Components = new OpenApiComponents(),
+ };
+ workingDocument.Components.Schemas = new Dictionary
+ {
+ ["Target"] = new OpenApiSchema
+ {
+ Maximum = "100",
+ Minimum = "1"
+ }
+ };
+ workingDocument.Workspace.RegisterComponents(workingDocument);
+
+ var schemaRef = new OpenApiSchemaReference("Target", workingDocument)
+ {
+ Maximum = "10",
+ Minimum = "5"
+ };
+
+ Assert.Equal("10", schemaRef.Maximum);
+ Assert.Equal("5", schemaRef.Minimum);
+ Assert.Equal("100", schemaRef.Target?.Maximum);
+ Assert.Equal("1", schemaRef.Target?.Minimum);
+
+ var relaxedSibling = new OpenApiSchemaReference("Target", workingDocument)
+ {
+ Maximum = "200",
+ Minimum = "0"
+ };
+
+ Assert.Equal("200", relaxedSibling.Maximum);
+ Assert.Equal("0", relaxedSibling.Minimum);
+ }
+
+ [Fact]
+ public void RequiredSiblingCollectionRemainsMutableAndStable()
+ {
+ var workingDocument = new OpenApiDocument
+ {
+ Components = new OpenApiComponents(),
+ };
+ workingDocument.Components.Schemas = new Dictionary
+ {
+ ["Target"] = new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ Required = new HashSet { "name" }
+ }
+ };
+ workingDocument.Workspace.RegisterComponents(workingDocument);
+
+ var schemaRef = new OpenApiSchemaReference("Target", workingDocument)
+ {
+ Required = new HashSet { "id" }
+ };
+
+ Assert.NotNull(schemaRef.Required);
+ Assert.Contains("id", schemaRef.Required);
+ Assert.DoesNotContain("name", schemaRef.Required);
+
+ schemaRef.Required.Add("email");
+
+ Assert.Contains("email", schemaRef.Required);
+ Assert.Contains("email", schemaRef.Reference.Required ?? new HashSet());
+ Assert.DoesNotContain("email", schemaRef.Target?.Required ?? new HashSet());
+ }
+
+ [Fact]
+ public void AdditionalPropertiesAllowedIsReferenceFirst()
+ {
+ // additionalProperties is location-sensitive and cannot be safely collapsed
+ // into a single effective boolean. Keep reference-first convenience behavior.
+ var workingDocument = new OpenApiDocument
+ {
+ Components = new OpenApiComponents(),
+ };
+ workingDocument.Components.Schemas = new Dictionary
+ {
+ ["TargetAPA"] = new OpenApiSchema { AdditionalPropertiesAllowed = true },
+ ["TargetNoAPA"] = new OpenApiSchema { AdditionalPropertiesAllowed = false },
+ };
+ workingDocument.Workspace.RegisterComponents(workingDocument);
+
+ // Sibling false overrides target true (reference-first).
+ var ref1 = new OpenApiSchemaReference("TargetAPA", workingDocument) { AdditionalPropertiesAllowed = false };
+ Assert.False(ref1.AdditionalPropertiesAllowed);
+
+ // No sibling → target fallback.
+ var ref2 = new OpenApiSchemaReference("TargetNoAPA", workingDocument);
+ Assert.False(ref2.AdditionalPropertiesAllowed);
+
+ // No sibling → target fallback.
+ var ref3 = new OpenApiSchemaReference("TargetAPA", workingDocument);
+ Assert.True(ref3.AdditionalPropertiesAllowed);
+ }
+
+ [Fact]
+ public void UnresolvedReferenceSiblingValuesAreReadable()
+ {
+ // When there is no host document, Target is null.
+ // Structural getters must fall back to the authored sibling values.
+ var schemaRef = new OpenApiSchemaReference("Pet", null)
+ {
+ Type = JsonSchemaType.String,
+ Format = "uuid",
+ MaxLength = 36,
+ Required = new HashSet { "id" }
+ };
+
+ Assert.Equal(JsonSchemaType.String, schemaRef.Type);
+ Assert.Equal("uuid", schemaRef.Format);
+ Assert.Equal(36, schemaRef.MaxLength);
+ Assert.Contains("id", schemaRef.Required ?? new HashSet());
+ }
+
+ [Fact]
+ public void AllOfSiblingCollectionRemainsMutableAndStable()
+ {
+ var workingDocument = new OpenApiDocument
+ {
+ Components = new OpenApiComponents(),
+ };
+ workingDocument.Components.Schemas = new Dictionary
+ {
+ ["Target"] = new OpenApiSchema
+ {
+ AllOf = new List
+ {
+ new OpenApiSchema { Type = JsonSchemaType.Object }
+ }
+ }
+ };
+ workingDocument.Workspace.RegisterComponents(workingDocument);
+
+ var schemaRef = new OpenApiSchemaReference("Target", workingDocument)
+ {
+ AllOf = new List
+ {
+ new OpenApiSchema { Type = JsonSchemaType.String }
+ }
+ };
+
+ Assert.NotNull(schemaRef.AllOf);
+ Assert.Single(schemaRef.AllOf);
+ Assert.Equal(JsonSchemaType.String, schemaRef.AllOf[0].Type);
+
+ schemaRef.AllOf.Add(new OpenApiSchema { Type = JsonSchemaType.Boolean });
+
+ Assert.Equal(2, schemaRef.AllOf.Count);
+ Assert.Equal(2, schemaRef.Reference.AllOf?.Count);
+ Assert.Single(schemaRef.Target?.AllOf ?? []);
+ }
}
}