Skip to content

[http-client-csharp] Preserve reader options when deserializing external models #11913

Description

@JoshLove-msft

Summary

Generated deserializers for properties whose model types come from another assembly hardcode ModelSerializationExtensions.WireOptions instead of forwarding the caller's ModelReaderWriterOptions. This silently drops unknown nested JSON properties during a JSON-persistence round-trip.

Found while reviewing Azure/azure-sdk-for-net#62269: Azure/azure-sdk-for-net#62269 (comment).

Generator evidence

In the unbranded C# generator, MrwSerializationTypeDefinition.DeserializeJsonValueCore accepts mrwOptions, but its fallback at lines 2518-2521 emits:

return ModelReaderWriterSnippets.Read(
    valueType,
    data,
    ModelSerializationExtensionsSnippets.Wire);

The Azure generator delegates deserialization of these external model types to the base generator; this is not an SDK-specific customization issue.

For example, after externalizing AzureAISearchToolOptions, the generated AzureAISearchToolboxTool deserializer changed from:

azureAiSearch = AzureAISearchToolOptions.DeserializeAzureAISearchToolOptions(prop.Value, options);

to:

azureAiSearch = ModelReaderWriter.Read<AzureAISearchToolOptions>(
    prop.Value.GetUtf8Bytes(),
    ModelSerializationExtensions.WireOptions,
    AzureAIProjectsAgentsContext.Default);

The same pattern occurs in nested collection/dictionary model reads, including ToolConfig in this serializer.

Reproduction

Reproduced against Azure/azure-sdk-for-net commit 6e80fb90609cf3f60b4a602c0337f17a81729982, targeting .NET 10, with Azure.AI.Extensions.OpenAI 3.0.0-alpha.20260908.3, OpenAI 2.12.0, and System.ClientModel 1.15.0.

using System;
using System.ClientModel.Primitives;
using Azure.AI.Projects.Agents;

var input = BinaryData.FromString(
    """{"type":"azure_ai_search","name":"search","azure_ai_search":{"indexes":[],"new_option":true}}""");

var model = ModelReaderWriter.Read<AzureAISearchToolboxTool>(
    input, ModelReaderWriterOptions.Json, AzureAIProjectsAgentsContext.Default);
var saved = ModelReaderWriter.Write(
    model, ModelReaderWriterOptions.Json, AzureAIProjectsAgentsContext.Default);

Console.WriteLine(saved.ToString());

Actual output:

{"type":"azure_ai_search","name":"search","azure_ai_search":{"indexes":[]}}

new_option is lost even though both outer operations use ModelReaderWriterOptions.Json. The nested model preserves unknown properties only outside wire mode, so forcing W prevents it from retaining that data.

Expected behavior

Forward the enclosing deserializer's options/mrwOptions into nested ModelReaderWriter.Read calls. JSON persistence should preserve unknown nested properties; wire-mode callers should retain their existing wire behavior.

Please cover external models in scalar properties, lists, and dictionaries, including unknown nested properties under JSON mode and unchanged behavior under wire mode.

Related but distinct: #9800 covered forwarding options to custom deserialization hooks, not external-model ModelReaderWriter.Read calls.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

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