microsoft / microsoft/typespec

[http-client-csharp] Optional nullable properties cannot emit explicit JSON null

Open
#11,970 1 comment 1 reaction 1 assignee Claimed by @JoshLove-msft View on GitHub
bug emitter:client:csharp
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.