Repository navigation
[http-client-csharp] Renamed property deserialization emits undeclared variable and default constructor argument with a back-compat version #12148
Description
Summary
When a backward-compatibility version is specified, generating a C# model with a renamed property can produce a deserializer that assigns an undeclared local variable and passes default instead of the parsed value to the serialization constructor.
The back-compat version is an essential reproduction condition, not an incidental setting. Please reproduce this with the released API contract loaded; a generation case without a back-compat version does not exercise the reported scenario. A separate no-back-compat comparison was not executed during this investigation.
Concrete example
Observed in Azure.ResourceManager.Network, using the management emitter 1.0.0-alpha.20260929.3 with released 1.17.0 as the compatibility baseline. The inspected management generator's PDB SourceLink identifies source commit 8a365a71da2d56bbe48e143264516f2d0e21820b in Azure/azure-sdk-for-net.
TypeSpec:
model VirtualNetworkEncryption {
enabled: boolean;
enforcement?: VirtualNetworkEncryptionEnforcement;
}
@@clientName(Common.VirtualNetworkEncryption.enabled, "IsEnabled", "csharp");The generated model exposes IsEnabled, retaining JSON name enabled. Its generated constructors include:
public VirtualNetworkEncryption(bool enabled)
{
IsEnabled = enabled;
}
internal VirtualNetworkEncryption(
bool enabled,
VirtualNetworkEncryptionEnforcement? enforcement,
IDictionary<string, BinaryData> additionalBinaryDataProperties)
{
IsEnabled = enabled;
Enforcement = enforcement;
_additionalBinaryDataProperties = additionalBinaryDataProperties;
}A custom public Enabled alias forwards to IsEnabled to preserve the released 1.17.0 API. There is no custom constructor or CodeGenMember mapping in the remaining customization:
public bool Enabled
{
get => IsEnabled;
set => IsEnabled = value;
}The pre-migration 1.15.0 API exposed only IsEnabled, whereas 1.17.0 exposed both Enabled and IsEnabled. The alias therefore preserves the loaded released contract; it is not independent model storage.
Actual generated deserializer
After removing a handwritten deserializer override and regenerating, VirtualNetworkEncryption.Serialization.cs contains the following pattern:
VirtualNetworkEncryptionEnforcement? enforcement = default;
IDictionary<string, BinaryData> additionalBinaryDataProperties =
new ChangeTrackingDictionary<string, BinaryData>();
// Inside the JSON-property loop:
if (prop.NameEquals("enabled"u8))
{
enabled = prop.Value.GetBoolean(); // No declaration for enabled exists.
continue;
}
// ...
return new VirtualNetworkEncryption(
default, enforcement, additionalBinaryDataProperties);This has two defects:
- Compilation fails with CS0103 because
enabledis undeclared. - The parsed value is discarded and the constructor receives
false. Restoring only the declaration would leave the data-loss bug intact.
A targeted SDK compilation confirmed the error. No runtime tests were run because the generated source does not compile.
The same regeneration also produced an analogous failure in RoutingRuleRouteDestination, where a property renamed to DestinationType leaves an assignment to undeclared @type and a default constructor argument. The detailed investigation so far focuses on VirtualNetworkEncryption.
Investigation and ownership boundary
Inspection of the installed management generator binaries identified a relevant downstream repair path:
ManagementClientGenerator.GetWritercallsSerializationConstructorCallHelper.FixConstructorCalls.- That helper rebuilds constructor arguments using parameter-name matching and substitutes defaults for unmatched parameters.
- Its property-identity fallback is restricted to renamed DateTime properties.
- It collects discarded argument variables and removes their declarations without repairing assignments in the JSON-property loop.
That path can produce the observed missing-declaration/default-argument pattern. However, the exact intermediate variable/property associations entering it, and the contribution of the retained alias, have not been isolated. These findings do not establish that TypeSpec core alone, without the Azure management generator integration, emits the same failure. Please investigate the back-compat constructor/property association across the base generator and management integration, and coordinate the fix in the owning layer.
Expected behavior
With the back-compat baseline enabled, preserve the released public API while consistently associating the parsed enabled value with the native IsEnabled property and serialization-constructor argument. The generated reader should declare the local, assign it, and pass it to the constructor without dropping the value.
Please add regression coverage with a back-compat version specified, renamed Boolean/enum properties, and a retained compatibility alias. Verify compilation and correct deserialization of both true and false; include a no-back-compat control. The fix should not require replacing the entire generated deserializer with handwritten SDK code.
Related SDK work
Azure/azure-sdk-for-net#63465. The deserializer-removal experiment is local and uncommitted; the current published SDK PR still contains the handwritten override, so its published generated source is not the failing reproduction.
Additional investigation clue: restored parameter names
The reporter suspects an interaction with the backward-compatibility feature that restores parameter names in methods/constructors. This is a debugging hypothesis, not a confirmed root cause.
The generated public constructor still uses the released parameter name enabled:
public VirtualNetworkEncryption(bool enabled)
{
IsEnabled = enabled;
}Meanwhile, the preferred C# property name is IsEnabled (JSON name remains enabled). Please trace whether restoring the historical constructor parameter name leaves deserialization locals/property associations using a different name, and whether the subsequent serialization-constructor argument repair matches against those restored names. Such a mismatch could explain both the discarded local declaration and the replacement of its constructor argument with default.
Investigate the analogous type / DestinationType case as well. Preserve the back-compat-version condition when reproducing this interaction; parameter-name restoration has not been isolated as the cause yet.
Suggested compatibility boundary
Consider limiting historical parameter-name restoration to externally accessible API members (public and protected, as applicable). Public parameter names matter for source compatibility, including callers using named arguments.
Internal serialization constructors do not expose their parameter names as a supported consumer API contract. They need not preserve historical names merely for public API compatibility; instead, their parameter names/associations can follow the current generated property model, with generated call sites updated consistently.
If parameter-name restoration is confirmed to contribute to this bug, excluding internal serialization constructors from that feature may avoid the mismatch without weakening public API compatibility. This is a proposed design direction, not a verified fix. Preserve any explicit custom-constructor associations and check for other generator dependencies before changing the behavior.
🤖 arcturus-copilot
Activity
github-actions commentedon Oct 9, 2026
🎯 Issue Triage
With a released API compatibility baseline loaded, a renamed C# model property can lose its serialization-constructor association, producing an undeclared deserialization local and passing
defaultinstead of the parsed value. This is a compile-time failure with a latent data-loss defect, observed through the management generator integration.Category: Bug
Area:
emitter:client:csharp🔗 [Playground Reproduction]((typespec.io/redacted)
📋 Reproduction code
🔍 Analysis
The affected path is C# model deserialization under back-compat contract loading, especially the management integration's
SerializationConstructorCallHelper.FixConstructorCalls. Parameter-name matching appears unable to associate the renamed native property with the existing constructor argument; it then substitutesdefaultand removes the argument variable declaration without rewriting the JSON-loop assignment. The result is both CS0103 and loss of the parsed Boolean/enum value. The retained compatibility alias may contribute to identity resolution, so ownership should be isolated between the base C# generator and the management integration. Searches found related back-compat serialization and rename issues, but none describing this same root cause. Related downstream work: Azure/azure-sdk-for-net#63465.📝 Suggested next steps
FixConstructorCalls.trueandfalse.Isolate C# back-compat parameter renaming from serialization (microso…