Conversation
Co-authored-by: baywet <7905502+baywet@users.noreply.github.com>
…3.2; add SerializeAsV32 with loop detection - Add Extensions property to JsonSchemaReference (serialize in v3.1/v3.2 only) - Add Extensions setter to OpenApiSchemaReference (checks reference-level extensions first) - Add SerializeAsV32 override to OpenApiSchemaReference with loop detection - Read extension properties from $ref sibling keywords during v3.1 deserialization - Add tests for extensions in v3.1/v3.2 and verify extensions dropped in v3.0/v2 Co-authored-by: baywet <7905502+baywet@users.noreply.github.com>
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
…ema references Signed-off-by: Vincent Biret <vibiret@microsoft.com>
| foreach (var property in jsonObject) | ||
| { | ||
| if (property.Key.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase) | ||
| && property.Value is JsonNode extensionValue) | ||
| { | ||
| Extensions ??= new Dictionary<string, IOpenApiExtension>(StringComparer.OrdinalIgnoreCase); | ||
| Extensions[property.Key] = new JsonNodeExtension(extensionValue.DeepClone()); | ||
| } | ||
| } |
Check notice
Code scanning / CodeQL
Missed opportunity to use Where Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 22 hours ago
In general, to fix this kind of issue you replace a foreach over the entire collection plus an inner conditional that skips most elements with a foreach whose source sequence has already been filtered using .Where(...). The condition in the if moves into the Where predicate. If there are additional conditions on the element (e.g., type checks), those also belong in the predicate when possible.
For this specific case in JsonSchemaReference.SetAdditional31MetadataFromMapNode, we should change the foreach (var property in jsonObject) loop to iterate over jsonObject.Where(...), with the predicate matching the original if condition:
property.Key.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase)
&& property.Value is JsonNode extensionValueInside the loop we still need access to extensionValue for constructing the JsonNodeExtension. Because the pattern-matching is expression that declares extensionValue cannot be lifted directly into the Where (without losing its scope), we can either (1) re-check/cast inside the loop or (2) use a slightly different LINQ pattern that projects the value. To keep the code minimal and not introduce new types, the least invasive fix is:
- Use
Whereonly for the key prefix filter and keep theis JsonNode extensionValuepattern check inside the loop. - This still reduces nesting and makes the intent clearer while preserving behavior and avoiding more complex LINQ expressions.
Concretely:
- Modify lines 159–167 to iterate over
jsonObject.Where(property => property.Key.StartsWith(...)). - Keep the
is JsonNode extensionValuepattern inside the loop body exactly as before. - No new imports or helpers are required;
System.Linqis already imported at the top of the file.
| @@ -156,10 +156,9 @@ | ||
| } | ||
|
|
||
| // Extensions (properties starting with "x-") | ||
| foreach (var property in jsonObject) | ||
| foreach (var property in jsonObject.Where(p => p.Key.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase))) | ||
| { | ||
| if (property.Key.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase) | ||
| && property.Value is JsonNode extensionValue) | ||
| if (property.Value is JsonNode extensionValue) | ||
| { | ||
| Extensions ??= new Dictionary<string, IOpenApiExtension>(StringComparer.OrdinalIgnoreCase); | ||
| Extensions[property.Key] = new JsonNodeExtension(extensionValue.DeepClone()); | ||
| @@ -167,3 +165,4 @@ | ||
| } | ||
| } | ||
| } | ||
|
|
|



Per OpenAPI spec, sibling keywords alongside
$refare version-dependent. For v2/v3.0 all siblings must be dropped; for v3.1/v3.2 schema refs, peer keywords including extensions are allowed; for v3.1/v3.2 non-schema refs, onlysummaryanddescriptionare permitted. This PR addresses the gap in extension support for schema references.Description
OpenApiSchemaReferencecorrectly drops sibling keywords (description, title, etc.) for v2/v3.0 serialization, but lacked support for serializing/deserializing extension properties (x-*) alongside$refin v3.1/v3.2, and was missing aSerializeAsV32override with loop detection.Type of Change
Related Issue(s)
Changes Made
JsonSchemaReferenceExtensionsproperty (IDictionary<string, IOpenApiExtension>?) — serialized only in v3.1/v3.2, silently dropped for v2/v3.0SerializeAdditionalV3XPropertiesnow callswriter.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_1)SetAdditional31MetadataFromMapNodenow parsesx-*properties from$refsibling keywords intoExtensionsOpenApiSchemaReferenceExtensionsupgraded from getter-only (Target?.Extensions) to get/set — reference-level extensions take priority over targetSerializeAsV32override with loop detection (matches existingSerializeAsV31pattern)PublicAPI.Unshipped.txtJsonSchemaReference.Extensions(get/set),OpenApiSchemaReference.Extensions.set,OpenApiSchemaReference.SerializeAsV32Tests
SerializeSchemaReferenceAsV31JsonWorksto include an extension; updated verified snapshotsSerializeSchemaReferenceAsV32JsonWorks+ snapshotsParseSchemaReferenceWithExtensionsWorks— parsesx-*from a v3.1$refnodeSchemaReferenceExtensionsNotWrittenInV30/SchemaReferenceExtensionsNotWrittenInV2— asserts only$refis emittedTesting
Checklist
Versions applicability
Additional Notes
Extension support is intentionally scoped to
JsonSchemaReference(schema refs only). Non-schema reference objects (OpenApiReferenceWithDescription,OpenApiReferenceWithDescriptionAndSummary) remain extension-free per the v3.1 Reference Object spec, which explicitly prohibits additional properties.Original prompt
This section details on the original issue you should resolve
<issue_title>OpenApiSchemaReference allows Description alongside $ref producing non-spec-compliant output in OAS 3.0 (clone of #2745)</issue_title>
<issue_description>## Description
The Microsoft.OpenApi library allows setting
DescriptiononOpenApiSchemaReference, which results in serialized output containing both$refanddescription.However, according to OpenAPI 3.0.x specification:
This makes
$refeffectively a replacement, meaning sibling keywords likedescriptionshould not appear.Example
Code
Output
Expected behavior
One of:
allOfDescription ignored when targeting OAS 3.0
Validation warning
Notes
In OAS 3.1 sibling keywords are allowed due to JSON Schema alignment
Behavior difference between 3.0 and 3.1 is currently unclear
This can lead to portability issues across tooling
This is a clone of OpenApiSchemaReference allows Description alongside $ref producing non-spec-compliant output in OAS 3.0 #2745
Question
What is the intended behavior when serializing references with annotations for OAS 3.0 vs 3.1?
</issue_description>
<agent_instructions>make sure you thoroughly consider my reply to implement the changes and tests</agent_instructions>
Comments on the Issue (you are @copilot in this section)
@baywet Hi @mdaneri,Thank you for using the SDK and for reaching out.
I think we need to break this down further:
OpenAPI 2 & 3.0
For any kind of reference (schema, path item, etc..), no sibling keyworks (description, summary, anything really) are allowed. We should drop them during serialization. Extension are not allowed either.
Source specification for v2
Source specification for v3
OpenAPI 3.1 & 3.2
For JSON Schema references, any keyword that's defined as a "peer keyword" can appear alongside the $ref property. That's effectively anything that has a setter in the reference type. Extensions are also allowed.
Source for schema object
For any other kind of reference (path item, etc...), ONLY summary and description are allowed alongside ...
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.