microsoft / microsoft/typespec
[http-client-csharp] Optional nullable properties cannot emit explicit JSON null
- Dominant language
- Java
- Stars
- 5.9k
- Forks
- 394
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 104
Description
## Description
The C# emitter collapses an optional nullable property's **omitted** and **explicitly null** states. Setting the generated property to `null` omits it from the request instead of writing JSON null.
This breaks services where omission selects a default but explicit null disables a feature. For example, Foundry voice-agent turn detection defaults to enabled and requires `"turn_detection": null` to disable it.
## Input and reproduction
The [pinned TypeSpec declarations](https://github.com/Azure/azure-rest-api-specs/blob/8a0bbe6c332a0d8b5423ac63e805e5d02eacfd60/specification/ai-foundry/data-plane/Foundry/src/voice-agents/agents.tsp#L437-L454) correctly model both optionality and nullability:
```typespec
noise_reduction?: VoiceAgentNoiseReduction | null;
turn_detection?: VoiceAgentTurnDetectionConfig | null;
transcription?: VoiceAgentInputTranscription | null;
```
Using the generated model in Azure/azure-sdk-for-net#62884:
```csharp
using System;
using System.ClientModel.Primitives;
using Azure.AI.Projects.Agents;
#pragma warning disable AAIP001
var input = new VoiceAgentAudioInputConfig
{
TurnDetection = null
};
Console.WriteLine(ModelReaderWriter.Write(
input,
new ModelReaderWriterOptions("W"),
AzureAIProjectsAgentsContext.Default).ToString());
```
**Actual:** `{}`
**Expected:** `{"turn_detection":null}`
Leaving `TurnDetection` unset should still produce `{}`. The generated model needs to preserve the distinction rather than serializing all unset nullable properties as null.
The same omission occurs for `NoiseReduction` and `Transcription`. Serialization through the containing voice-agent definition produces `"audio":{"input":{}}`, so this also affects actual agent request bodies. The omission was reproduced against the compiled SDK and its real dependencies without service calls.
## Upstream implementation
In the pinned generator, [`MrwSerializationTypeDefinition.CreateConditionalSerializationStatement`](https://github.com/microsoft/typespec/blob/9b8c51de8c15b6dc748ea9cf576d87f91ef058c4/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs#L2628-L2663) uses `Optional.IsDefined(property)` and generates an explicit-null branch only for `isRequired && isNullable`. Optional nullable properties fall through to an `if (IsDefined(...))` guard, and these generated reference-type properties have no explicit-presence tracking.
The SDK's active custom visitors only add experimental attributes or adjust `virtual`/`override` modifiers; they do not change these property types or serialization conditions.
## Environment
- Emitter: `@typespec/http-client-csharp` **1.0.0-alpha.20260910.2**
- Generator: `Microsoft.TypeSpec.Generator.ClientModel` **1.0.0-alpha.20260910.2**
- Generator source revision, verified from the NuGet package metadata: `9b8c51de8c15b6dc748ea9cf576d87f91ef058c4`
- SDK reproduction: Azure/azure-sdk-for-net#62884 at `efa46fdc6e3fca32a195fc575b873fd696a7b572`
- [Original SDK review finding](https://github.com/Azure/azure-sdk-for-net/pull/62884#discussion_r4018362046)
## Expected coverage
Please cover omitted, explicitly null, and non-null values for optional nullable model properties, including nested wire serialization and preservation of explicit null when reading and then writing a model.
Contributor guide
Assessment
This issue has not been assessed yet.