microsoft / microsoft/typespec

[Bug]: `@typespec/http-server-csharp` emits `[JsonConverter(typeof(JsonStringEnumConverter))]` on nullable string properties

Open
#10,603 3 comments 0 reactions 1 assignee Claimed by @markcowl View on GitHub
bug emitter:service:csharp triaged:core
Dominant language
Java
Stars
5.9k
Forks
394
Avg merge
1d 23h
Merged PRs (30d)
104

Description

### Describe the bug

> Disclaimer: this bug report is generated with the help of Copilot and manually verified.

## Package

`@typespec/http-server-csharp` v0.58.0-alpha.27

## Description

The C# server emitter incorrectly adds `[JsonConverter(typeof(JsonStringEnumConverter))]` to properties typed as `string | null`. This attribute is only valid for enum types in .NET. At runtime, it causes:

```
System.InvalidOperationException: The converter specified on 'Repro.Foo.Nullable' is not compatible with the type 'System.String'.
```

### Reproduction

## Minimal Reproduction

**main.tsp**

```typespec
import "@typespec/http";

using TypeSpec.Http;

@service(#{ title: "Repro" })
namespace Repro;

model Foo {
/** A plain string — should NOT get JsonStringEnumConverter */
required: string;

/** A nullable string (string | null) — should NOT get JsonStringEnumConverter */
nullable: string | null;

/** A true string enum — SHOULD get JsonStringEnumConverter */
color: "red" | "green" | "blue";
}

@route("/foo")
op getFoo(): Foo;
```

**tspconfig.yaml**

```yaml
emit:
- "@typespec/http-server-csharp"
```

**package.json**

```json
{
"name": "typespec-repro-nullable-string-enum",
"private": true,
"dependencies": {
"@typespec/http-server-csharp": "0.58.0-alpha.27"
}
}
```

Run: `npm install && npx tsp compile .`

### Actual Output (Foo.cs)

```csharp
public partial class Foo
{
[JsonPropertyName("required")]
public string RequiredName { get; set; }

[JsonConverter(typeof(JsonStringEnumConverter))] // ← BUG: invalid on string
public string Nullable { get; set; }

public string Color { get; set; }
}
```

### Expected Output

```csharp
public partial class Foo
{
[JsonPropertyName("required")]
public string RequiredName { get; set; }

public string? Nullable { get; set; } // No JsonStringEnumConverter

[JsonConverter(typeof(JsonStringEnumConverter))]
public string Color { get; set; } // This one is a real string enum
}
```

## Root Cause

In [`utils.ts`](https://github.com/microsoft/typespec/blob/d973bac7e4a8bd1817c077290967e3a19a980ec4/packages/http-server-csharp/src/lib/utils.ts#L1481), `isStringEnumType()` calls `coalesceUnionTypes()` which reduces `string | null` to `CSharpType("string", isBuiltIn=true)`. Since `baseType.name === "string"` and `baseType.isBuiltIn === true`, the first guard passes. The subsequent check iterates variants looking for unnamed string literals, but the `null` variant has `kind === "Intrinsic"`, so it doesn't match the `some()` predicate — causing `isStringEnumType` to return `true`.

A fix would be to exclude unions where all non-null variants are scalar/intrinsic types (i.e., unions that represent nullable scalars rather than string enums). For example:

```typescript
export function isStringEnumType(program: Program, union: Union): boolean {
const nonNullVariants = [...union.variants.values()].filter(v => !isNullType(v.type));
// A nullable scalar (string | null) is not a string enum
if (nonNullVariants.length <= 1) return false;
// ... existing logic
}
```

## Impact

Any model with a `string | null` property produces C# code that crashes at runtime with `InvalidOperationException` when the model is serialized or deserialized.

## Workaround

Post-process generated files to strip the invalid attribute:

```makefile
find tsp-output -name '*.cs' -exec sed -i \
'/\[JsonConverter(typeof(JsonStringEnumConverter))\]/{N;/public string /{s/^.*\[JsonConverter(typeof(JsonStringEnumConverter))\]\n//;}}' {} +
```

## Environment

- TypeSpec compiler: v1.11.0
- `@typespec/http-server-csharp`: v0.58.0-alpha.27
- .NET: 9.0
- OS: Linux (Ubuntu)

### Checklist

- [x] Follow our [Code of Conduct](https://github.com/microsoft/typespec/blob/main/CODE_OF_CONDUCT.md)
- [x] Check that there isn't already an issue that request the same bug to avoid creating a duplicate.
- [x] Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/Microsoft/typespec/discussions).
- [x] The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug.

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.