Skip to content

[Bug] Generator crashes on specs with $-prefixed OData parameter names in example values #9982

@haiyuazhang

Description

@haiyuazhang

Describe the bug

The C# generator crashes with a TypeInitializationException when processing TypeSpec specs that contain OData query parameters (e.g. $filter, $orderby) whose names appear as dictionary keys in example values within the serialized code model (tspCodeModel.json).

The crash occurs during code model deserialization, before any code generation begins. The trigger is this line in the spec, which defines a $filter property that ends up as a $-prefixed dictionary key in the serialized code model.

Error output

Emitter "@azure-typespec/http-client-csharp-mgmt" crashed! This is a bug.
Error: Failed to generate the library. Exit code: 1.
The type initializer for
'Microsoft.TypeSpec.Generator.ClientModel.Providers.ModelReaderWriterContextDefinition'
threw an exception.
   at Microsoft.TypeSpec.Generator.ClientModel.ScmCodeModelGenerator.Configure()
   at Azure.Generator.AzureClientGenerator.Configure()
   at Azure.Generator.Management.ManagementClientGenerator.Configure()
   at Microsoft.TypeSpec.Generator.GeneratorHandler.SelectGenerator(CommandLineOptions options)

The inner exception (hidden by TypeInitializationException) is:

System.Text.Json.JsonException: Properties that start with '$' are not allowed
in types that support metadata. Either replace the character or disable reference
preservation and polymorphic deserialization.
Path: $ | LineNumber: 98083 | BytePositionInLine: 42.

Root cause

The emitter serializes OData parameter names as JSON property keys in the code model. For example, the BillingBenefits spec produces:

"value": {
  "$filter": {
    "$id": "9016",
    "kind": "string",
    "type": { "$ref": "1498" },
    "value": "properties/status ne 'Canceled'"
  },
  "billingAccountResourceId": { ... }
}

The generator deserializes tspCodeModel.json using System.Text.Json with a TypeSpecReferenceHandler (which enables $id/$ref reference preservation). When ReferenceHandler is active, System.Text.Json rejects any property name starting with $ that is not $id or $ref.

The Utf8JsonReaderExtensions class has a TryReadComplexType overload for IReadOnlyList<T> that manually reads arrays via Utf8JsonReader (bypassing the default converter), but no equivalent overload for IReadOnlyDictionary<string, T>. The dictionary deserialization in TypeSpecInputExampleValueConverter.CreateObjectExample() falls through to the generic TryReadComplexType<T>, which delegates to the default System.Text.Json dictionary converter — and that converter enforces the $-prefix restriction.

Proposed fix

Add a TryReadComplexType overload for IReadOnlyDictionary<string, T> in Utf8JsonReaderExtensions.cs that manually reads dictionary entries using Utf8JsonReader, consistent with the existing IReadOnlyList<T> overload:

public static bool TryReadComplexType<T>(
    this ref Utf8JsonReader reader,
    string propertyName,
    JsonSerializerOptions options,
    ref IReadOnlyDictionary<string, T>? value)
{
    if (reader.TokenType != JsonTokenType.PropertyName)
        throw new JsonException();
    if (reader.GetString() != propertyName)
        return false;

    reader.Read();
    if (reader.TokenType != JsonTokenType.StartObject)
        throw new JsonException();
    reader.Read();

    var result = new Dictionary<string, T>();
    while (reader.TokenType != JsonTokenType.EndObject)
    {
        var key = reader.GetString()
            ?? throw new JsonException("Dictionary key cannot be null");
        reader.Read();
        var item = reader.ReadWithConverter<T>(options);
        result[key] = item ?? throw new JsonException();
    }
    reader.Read();
    value = result;
    return true;
}

C# overload resolution automatically selects this for IReadOnlyDictionary<string, T> callers (like CreateObjectExample), so no changes needed in the converter code.

Impact

Any TypeSpec spec with OData query parameters ($filter, $orderby, $top, $skip, etc.) that appear as dictionary keys in example values will crash the generator. Known affected spec: BillingBenefits.Management.

Environment

  • @typespec/http-client-csharp (root generator)
  • .NET 10.0.103
  • TypeSpec Compiler 1.9.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    emitter:client:csharpIssue for the C# client emitter: @typespec/http-client-csharp

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions