Skip to content

Commit 2727d30

Browse files
authored
Merge branch 'main' into dev/ygerges/const
2 parents 6b91e2b + eacc2fc commit 2727d30

22 files changed

Lines changed: 3593 additions & 261 deletions

src/Microsoft.OpenApi/Models/JsonSchemaReference.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,17 @@ public string? Const
347347
/// </summary>
348348
public IOpenApiSchema? Else { get; set; }
349349

350+
/// <summary>
351+
/// Indicates whether this reference represents a bare $dynamicRef (no $ref to a component).
352+
/// When true, serialization emits $dynamicRef instead of $ref, and Target resolution
353+
/// uses the $dynamicAnchor index rather than the $ref URI lookup.
354+
/// Computed from whether $dynamicRef is set and ReferenceV3 does not point to a component path.
355+
/// </summary>
356+
internal bool IsDynamicRefOnly
357+
=> !string.IsNullOrEmpty(DynamicRef)
358+
&& ReferenceV3 is string refV3
359+
&& !refV3.StartsWith("#/components/", StringComparison.OrdinalIgnoreCase);
360+
350361
/// <summary>
351362
/// Parameterless constructor
352363
/// </summary>
@@ -422,6 +433,35 @@ public JsonSchemaReference(JsonSchemaReference reference) : base(reference)
422433
Else = reference.Else;
423434
}
424435

436+
/// <inheritdoc/>
437+
public override void SerializeAsV31(IOpenApiWriter writer)
438+
{
439+
if (IsDynamicRefOnly)
440+
{
441+
writer.WriteStartObject();
442+
SerializeAdditionalV3XProperties(writer, (w, e) => e.SerializeAsV31(w), base.SerializeAdditionalV31Properties);
443+
writer.WriteEndObject();
444+
}
445+
else
446+
{
447+
base.SerializeAsV31(writer);
448+
}
449+
}
450+
/// <inheritdoc/>
451+
public override void SerializeAsV32(IOpenApiWriter writer)
452+
{
453+
if (IsDynamicRefOnly)
454+
{
455+
writer.WriteStartObject();
456+
SerializeAdditionalV3XProperties(writer, (w, e) => e.SerializeAsV32(w), base.SerializeAdditionalV32Properties);
457+
writer.WriteEndObject();
458+
}
459+
else
460+
{
461+
base.SerializeAsV32(writer);
462+
}
463+
}
464+
425465
/// <inheritdoc/>
426466
protected override void SerializeAdditionalV31Properties(IOpenApiWriter writer)
427467
{
@@ -432,6 +472,7 @@ protected override void SerializeAdditionalV32Properties(IOpenApiWriter writer)
432472
{
433473
SerializeAdditionalV3XProperties(writer, (w, e) => e.SerializeAsV32(w), base.SerializeAdditionalV32Properties);
434474
}
475+
435476
private void SerializeAdditionalV3XProperties(IOpenApiWriter writer, Action<IOpenApiWriter, IOpenApiSerializable> serializeCallback, Action<IOpenApiWriter> baseSerializer)
436477
{
437478
if (Type != ReferenceType.Schema) throw new InvalidOperationException(

src/Microsoft.OpenApi/Models/OpenApiDiscriminator.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public void SerializeAsV32(IOpenApiWriter writer)
5858
if (DefaultMapping != null)
5959
{
6060
writer.WritePropertyName("defaultMapping");
61-
DefaultMapping.SerializeAsV32(writer);
61+
WriteMappingReference(writer, DefaultMapping);
6262
}
6363

6464
// extensions
@@ -79,7 +79,7 @@ public void SerializeAsV31(IOpenApiWriter writer)
7979
if (DefaultMapping != null)
8080
{
8181
writer.WritePropertyName("x-oas-default-mapping");
82-
DefaultMapping.SerializeAsV31(writer);
82+
WriteMappingReference(writer, DefaultMapping);
8383
}
8484

8585
// extensions
@@ -108,13 +108,16 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
108108
writer.WriteProperty(OpenApiConstants.PropertyName, PropertyName);
109109

110110
// mapping
111-
writer.WriteOptionalMap(OpenApiConstants.Mapping, Mapping, (w, s) =>
111+
writer.WriteOptionalMap(OpenApiConstants.Mapping, Mapping, WriteMappingReference);
112+
}
113+
114+
private static void WriteMappingReference(IOpenApiWriter writer, OpenApiSchemaReference schemaReference)
115+
{
116+
var reference = schemaReference.Reference.ReferenceV3;
117+
if (!string.IsNullOrEmpty(reference))
112118
{
113-
if (!string.IsNullOrEmpty(s.Reference.ReferenceV3) && s.Reference.ReferenceV3 is not null)
114-
{
115-
w.WriteValue(s.Reference.ReferenceV3);
116-
}
117-
});
119+
writer.WriteValue(reference!);
120+
}
118121
}
119122

120123
/// <summary>

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

Lines changed: 34 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ namespace Microsoft.OpenApi
2121
/// </summary>
2222
public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaMissingProperties, IOpenApiSchemaWithUnevaluatedProperties, IMetadataContainer
2323
{
24+
private static readonly IEnumerable<JsonNode> s_singleNullElementList = [ JsonNullSentinel.JsonNull ];
25+
2426
/// <inheritdoc />
2527
public string? Title { get; set; }
2628

@@ -109,13 +111,8 @@ public string? ExclusiveMinimum
109111
/// <inheritdoc />
110112
public JsonSchemaType? Type { get; set; }
111113

112-
// x-nullable is filtered out by deserializers, but keep the check here in case it gets added from user code.
113-
private bool IsNullable =>
114-
(Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null)) ||
115-
Extensions is not null &&
116-
Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) &&
117-
nullExtRawValue is JsonNodeExtension { Node: JsonNode jsonNode } &&
118-
jsonNode.GetValueKind() is JsonValueKind.True;
114+
private bool HasNullType
115+
=> Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null);
119116

120117
internal bool WasConstExplicitlySet { get; private set; }
121118

@@ -538,57 +535,30 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
538535
: Enum;
539536
writer.WriteOptionalCollection(OpenApiConstants.Enum, enumValue, (nodeWriter, s) => nodeWriter.WriteAny(s));
540537

541-
// Handle oneOf/anyOf with null type for v3.0 downcast
542-
IList<IOpenApiSchema>? effectiveOneOf = OneOf;
543-
IList<IOpenApiSchema>? effectiveAnyOf = AnyOf;
544-
bool hasNullInComposition = false;
545-
bool hasOneOfNullAndSingleEnumWith3_0 = false;
546-
JsonSchemaType? inferredType = null;
547-
548538
if (version == OpenApiSpecVersion.OpenApi3_0)
549539
{
550-
(effectiveOneOf, var inferredOneOf, var nullInOneOf) = ProcessCompositionForNull(OneOf);
551-
hasNullInComposition |= nullInOneOf;
552-
inferredType = inferredOneOf ?? inferredType;
553-
(effectiveAnyOf, var inferredAnyOf, var nullInAnyOf) = ProcessCompositionForNull(AnyOf);
554-
hasNullInComposition |= nullInAnyOf;
555-
inferredType = inferredAnyOf ?? inferredType;
556-
557-
hasOneOfNullAndSingleEnumWith3_0 = nullInOneOf && effectiveOneOf is { Count: 1 } &&
558-
effectiveOneOf[0].Enum is { Count: > 0 };
540+
// If we have a schema that's only just { "type": "null" }, we serialize it as enum with null value.
541+
if (Type == JsonSchemaType.Null &&
542+
OneOf is not { Count: > 0 } &&
543+
AnyOf is not { Count: > 0 } &&
544+
AllOf is not { Count: > 0 } &&
545+
Enum is not { Count: > 0 })
546+
{
547+
writer.WriteOptionalCollection(OpenApiConstants.Enum, s_singleNullElementList, (nodeWriter, s) => nodeWriter.WriteAny(s));
548+
}
559549
}
560550

561551
// type
562-
SerializeTypeProperty(writer, version, inferredType);
552+
var serializedTypeProperty = TrySerializeTypeProperty(writer, version);
563553

564554
// allOf
565555
writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, callback);
566556

567557
// anyOf
568-
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, effectiveAnyOf, callback);
558+
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, AnyOf, callback);
569559

570560
// oneOf
571-
if (hasOneOfNullAndSingleEnumWith3_0)
572-
{
573-
writer.WriteRequiredCollection(OpenApiConstants.OneOf, effectiveOneOf!, (writer, element) =>
574-
{
575-
var clonedToMutateEnum = element.CreateShallowCopy();
576-
if (clonedToMutateEnum is OpenApiSchema { Enum: { } existingEnum } concreteCloned)
577-
{
578-
concreteCloned.Enum = [.. existingEnum, JsonNullSentinel.JsonNull];
579-
callback(writer, clonedToMutateEnum);
580-
}
581-
else
582-
{
583-
callback(writer, element);
584-
}
585-
});
586-
}
587-
else
588-
{
589-
writer.WriteOptionalCollection(OpenApiConstants.OneOf, effectiveOneOf, callback);
590-
}
591-
561+
writer.WriteOptionalCollection(OpenApiConstants.OneOf, OneOf, callback);
592562

593563
// not
594564
writer.WriteOptionalObject(OpenApiConstants.Not, Not, callback);
@@ -624,9 +594,14 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
624594
writer.WriteOptionalObject(OpenApiConstants.Default, Default, (w, d) => w.WriteAny(d));
625595

626596
// nullable
627-
if (version == OpenApiSpecVersion.OpenApi3_0)
597+
if (version == OpenApiSpecVersion.OpenApi3_0 && serializedTypeProperty)
628598
{
629-
SerializeNullable(writer, version, hasNullInComposition);
599+
// https://spec.openapis.org/oas/v3.0.4.html#fixed-fields-20
600+
// This keyword only takes effect if type is explicitly defined within the same Schema Object.
601+
//
602+
// If the user explicitly set IsNullable to true, we serialize it even if redundant.
603+
// But if **we** are inferring it (from oneOf/anyOf), we don't serialize it when it's redundant.
604+
SerializeNullable(writer, version);
630605
}
631606

632607
// discriminator
@@ -858,7 +833,7 @@ private void SerializeAsV2(
858833
writer.WriteStartObject();
859834

860835
// type
861-
SerializeTypeProperty(writer, OpenApiSpecVersion.OpenApi2_0);
836+
TrySerializeTypeProperty(writer, OpenApiSpecVersion.OpenApi2_0);
862837

863838
// description
864839
writer.WriteProperty(OpenApiConstants.Description, Description);
@@ -1021,31 +996,32 @@ private void SerializeAsV2(
1021996
writer.WriteEndObject();
1022997
}
1023998

1024-
private void SerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion version, JsonSchemaType? inferredType = null)
999+
private bool TrySerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion version, JsonSchemaType? inferredType = null)
10251000
{
10261001
// Use original type or inferred type when the explicit type is not set
10271002
var typeToUse = Type ?? inferredType;
10281003

10291004
if (typeToUse is null)
10301005
{
1031-
return;
1006+
return false;
10321007
}
10331008

1034-
var unifiedType = IsNullable ? typeToUse.Value | JsonSchemaType.Null : typeToUse.Value;
1035-
var typeWithoutNull = unifiedType & ~JsonSchemaType.Null;
1036-
10371009
switch (version)
10381010
{
10391011
case OpenApiSpecVersion.OpenApi2_0 or OpenApiSpecVersion.OpenApi3_0:
1012+
var typeWithoutNull = typeToUse.Value & ~JsonSchemaType.Null;
10401013
if (typeWithoutNull != 0 && !HasMultipleTypes(typeWithoutNull))
10411014
{
10421015
writer.WriteProperty(OpenApiConstants.Type, typeWithoutNull.ToFirstIdentifier());
1016+
return true;
10431017
}
10441018
break;
10451019
default:
1046-
WriteUnifiedSchemaType(unifiedType, writer);
1047-
break;
1020+
WriteUnifiedSchemaType(typeToUse.Value, writer);
1021+
return true;
10481022
}
1023+
1024+
return false;
10491025
}
10501026

10511027
private static bool IsPowerOfTwo(int x)
@@ -1080,9 +1056,9 @@ where type.HasFlag(flag)
10801056
}
10811057
}
10821058

1083-
private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version, bool hasNullInComposition = false)
1059+
private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version)
10841060
{
1085-
if (IsNullable || hasNullInComposition)
1061+
if (HasNullType)
10861062
{
10871063
switch (version)
10881064
{
@@ -1096,63 +1072,6 @@ private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version
10961072
}
10971073
}
10981074

1099-
/// <summary>
1100-
/// Processes a composition (oneOf or anyOf) for null types, filtering out null schemas and inferring common type.
1101-
/// </summary>
1102-
/// <param name="composition">The list of schemas in the composition.</param>
1103-
/// <returns>A tuple with the effective list, inferred type, and whether null is present in composition.</returns>
1104-
private static (IList<IOpenApiSchema>? effective, JsonSchemaType? inferredType, bool hasNullInComposition)
1105-
ProcessCompositionForNull(IList<IOpenApiSchema>? composition)
1106-
{
1107-
if (composition is null || !composition.Any(static s => s.Type is JsonSchemaType.Null))
1108-
{
1109-
// Nothing to patch
1110-
return (composition, null, false);
1111-
}
1112-
1113-
var nonNullSchemas = composition
1114-
.Where(static s => s.Type is null or not JsonSchemaType.Null)
1115-
.ToList();
1116-
1117-
if (nonNullSchemas.Count > 0)
1118-
{
1119-
JsonSchemaType commonType = 0;
1120-
1121-
foreach (var schema in nonNullSchemas)
1122-
{
1123-
if (schema.Type.HasValue)
1124-
{
1125-
commonType |= schema.Type.Value & ~JsonSchemaType.Null;
1126-
}
1127-
else if (schema.Enum is { Count: > 0 })
1128-
{
1129-
foreach (var enumValue in schema.Enum.Where(x => x is not null))
1130-
{
1131-
var currentType = enumValue.GetValueKind() switch
1132-
{
1133-
JsonValueKind.Array => JsonSchemaType.Array,
1134-
JsonValueKind.String => JsonSchemaType.String,
1135-
JsonValueKind.Number => JsonSchemaType.Number,
1136-
JsonValueKind.True or JsonValueKind.False => JsonSchemaType.Boolean,
1137-
JsonValueKind.Null => (JsonSchemaType)0,
1138-
_ => JsonSchemaType.Object,
1139-
};
1140-
1141-
commonType |= currentType;
1142-
}
1143-
1144-
commonType |= JsonSchemaType.String;
1145-
}
1146-
}
1147-
1148-
return (nonNullSchemas, commonType == 0 ? null : commonType, true);
1149-
}
1150-
else
1151-
{
1152-
return (null, null, true);
1153-
}
1154-
}
1155-
11561075
#if NET5_0_OR_GREATER
11571076
private static readonly Array jsonSchemaTypeValues = System.Enum.GetValues<JsonSchemaType>();
11581077
#else

src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,26 @@ private OpenApiSchemaReference(OpenApiSchemaReference schema) : base(schema)
3535
{
3636
}
3737

38+
/// <summary>
39+
/// Resolves the target schema. For $dynamicRef-only references, delegates to
40+
/// <see cref="OpenApiWorkspace.ResolveDynamicRef"/> which resolves via per-document
41+
/// $dynamicAnchor and $anchor registries. Returns null when anchors are ambiguous
42+
/// (multiple candidates require dynamic-scope tracking this library does not perform).
43+
/// </summary>
44+
public override IOpenApiSchema? Target
45+
{
46+
get
47+
{
48+
if (Reference.IsDynamicRefOnly
49+
&& Reference.HostDocument is { } doc
50+
&& doc.Workspace is { } ws)
51+
{
52+
return ws.ResolveDynamicRef(doc, Reference.DynamicRef!);
53+
}
54+
return base.Target;
55+
}
56+
}
57+
3858
/// <inheritdoc/>
3959
public string? Description
4060
{
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
#nullable enable
2+
override Microsoft.OpenApi.JsonSchemaReference.SerializeAsV31(Microsoft.OpenApi.IOpenApiWriter! writer) -> void
3+
override Microsoft.OpenApi.JsonSchemaReference.SerializeAsV32(Microsoft.OpenApi.IOpenApiWriter! writer) -> void
4+
override Microsoft.OpenApi.OpenApiSchemaReference.Target.get -> Microsoft.OpenApi.IOpenApiSchema?
5+
Microsoft.OpenApi.OpenApiWorkspace.GetDynamicAnchorCandidates(Microsoft.OpenApi.OpenApiDocument! hostDocument, string! anchorName) -> System.Collections.Generic.IReadOnlyList<Microsoft.OpenApi.IOpenApiSchema!>!
6+
static Microsoft.OpenApi.OpenApiWorkspace.ResolveDynamicAnchorInContext(Microsoft.OpenApi.IOpenApiSchema? contextSchema, string! anchorName) -> Microsoft.OpenApi.IOpenApiSchema?

0 commit comments

Comments
 (0)