Skip to content

Fix XML serialization in convenience methods for dual-format models#9712

Open
Copilot wants to merge 15 commits intomainfrom
copilot/add-xml-serialization-logic
Open

Fix XML serialization in convenience methods for dual-format models#9712
Copilot wants to merge 15 commits intomainfrom
copilot/add-xml-serialization-logic

Conversation

Copy link
Contributor

Copilot AI commented Feb 13, 2026

Add logic to use XML serialization/deserialization in protocol methods based on the content-type

Plan:

  • Understand the issue and analyze the current implementation
  • Add internal ToBinaryContent(string format) helper method to model serialization
  • Update convenience method generation to detect content-type and use appropriate format
  • Generate using statements for proper BinaryContent disposal
  • Verify tests pass and validate generated code
  • Add test scenario with Tree model used in both JSON and XML operations
  • Address all code review feedback
  • Add comprehensive unit tests with TestData validation
  • Remove unused code and simplify test code
  • Final code review and validation
  • All review comments addressed ✅

Implementation Summary:

Problem:
When a model supports both JSON and XML serialization (e.g., Tree model with @usage(Usage.xml | Usage.output | Usage.json)), convenience methods were always using the wire format ("J" for JSON) through the implicit operator, even when the operation specified @header contentType: "application/xml". This caused runtime failures when XML-formatted content was expected.

Solution:

  1. Added internal ToBinaryContent(string format) method to models that support both JSON and XML:

    • Located in MrwSerializationTypeDefinition.cs via BuildToBinaryContentMethod()
    • Method signature: internal BinaryContent ToBinaryContent(string format)
    • Creates ModelReaderWriterOptions with the specified format ("J" or "X")
    • Calls BinaryContent.Create(this, options) to serialize with the correct format
    • Method name and description dynamically use RequestContentType.Name
  2. Updated convenience method code generation in ScmMethodProviderCollection.cs:

    • Added logic to GetStackVariablesForProtocolParamConversion to generate using declarations for proper resource disposal
    • Loops through operation's RequestMediaTypes to find JSON or XML content-type
    • Uses helper method TryGetFormatArgumentForDualFormatModel with [NotNullWhen(true)] for null safety
    • Uses constants JsonMediaType and XmlMediaType for media type strings
    • For body parameters that are models:
      • Checks if model supports both JSON and XML using cached model information
      • If dual-format model and operation specifies content-type:
        • Generates using BinaryContent content = parameter.ToBinaryContent("X") for XML operations
        • Generates using BinaryContent content = parameter.ToBinaryContent("J") for JSON operations
        • Uses ModelReaderWriterOptionsSnippets.XmlFormat and .JsonFormat for format values
      • Method invocation is dynamic using To{RequestContentType.Name} pattern
      • Otherwise uses implicit operator (existing behavior)
    • Removed unused variables for cleaner code

Example Generated Code:

// XML operation:
public virtual ClientResult<Tree> UpdateTree(Tree tree, CancellationToken cancellationToken = default)
{
    Argument.AssertNotNull(tree, nameof(tree));
    
    using BinaryContent content = tree.ToBinaryContent("X");
    ClientResult result = UpdateTree(content, cancellationToken.ToRequestOptions());
    return ClientResult.FromValue((Tree)result, result.GetRawResponse());
}

// JSON operation:
public virtual ClientResult<Tree> UpdateTreeAsJson(Tree tree, CancellationToken cancellationToken = default)
{
    Argument.AssertNotNull(tree, nameof(tree));
    
    using BinaryContent content = tree.ToBinaryContent("J");
    ClientResult result = UpdateTreeAsJson(content, cancellationToken.ToRequestOptions());
    return ClientResult.FromValue((Tree)result, result.GetRawResponse());
}

// Single-format operation (no ToBinaryContent):
public virtual ClientResult<Thing> CreateLiteral(Thing body, CancellationToken cancellationToken = default)
{
    Argument.AssertNotNull(body, nameof(body));
    
    return CreateLiteral(body, cancellationToken.ToRequestOptions());
}

Testing:

  • ✅ All tests pass (1147 ClientModel tests, 1244 generator tests, 838 spector tests, 75 input tests, 37 local tests = 3,341 total, 0 failures)
  • ✅ Code review passed with no issues
  • Added comprehensive unit tests (8 new tests):
    • TreeXmlTests (2 tests): Uses reflection to call internal ToBinaryContent method, directly accesses public MediaType property (application/json for "J", null/empty for "X")
    • MrwSerializationTypeDefinitionTests (3 tests): Validates ToBinaryContent method generation for dual-format models (positive) and single-format models (negative)
    • ScmMethodProviderCollectionTests (3 tests): Validates complete convenience method code using TestData files for XML/JSON dual-format and single-format scenarios
  • Generated code verified for Tree model with both XML and JSON operations
  • Sample-TypeSpec project demonstrates dual-format scenario with proper resource disposal:
    • Tree model supports both JSON and XML (default usage)
    • UpdateTree generates: using BinaryContent content = tree.ToBinaryContent("X");
    • UpdateTreeAsJson generates: using BinaryContent content = tree.ToBinaryContent("J");

Security Summary

No security vulnerabilities introduced. Changes are limited to code generation logic that:

  • Adds proper resource disposal (using statements)
  • Uses existing ModelReaderWriterOptions and BinaryContent APIs
  • No new external dependencies or network calls
Original prompt

This section details on the original issue you should resolve

<issue_title>Add logic to use XML serialization/deserialization in protocol methods based on the content-type</issue_title>
<issue_description>If Content-Type is application/xml, then we should use xml serialization. Will need to convert models to/from BinaryContent using the xml serialization.

Edit:
We can construct an internal serialization helper to have the model serialize into either XML or JSON depending on the format requested:

internal BinaryContent ToBinaryContent(string format)
{
    ModelReaderWriterOptions options = new ModelReaderWriterOptions(format);
    return BinaryContent.Create(this, options);
}

Then in the convenience method, we can supply the format depending on the operation's content type. "J" for json and "X" for xml content type:

ClientResult result = UpdateTree(tree.ToBinaryContent("X"), cancellationToken.ToRequestOptions());
```</issue_description>

## Comments on the Issue (you are @copilot in this section)

<comments>
<comment_new><author>@JoshLove-msft</author><body>
Blocked on the XML serialization support.</body></comment_new>
<comment_new><author>@jorgerangel-msft</author><body>
@JoshLove-msft do you have more details on what this should entail? In the rare case an operation would support both json and xml, I have the content type check on the operator used for deserialization https://github.com/microsoft/typespec/blob/main/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/Tree.Serialization.cs#L103. For serializing to BinaryContent, it's a bit more tricky to do this via an operator since we don't have the contenttype info at runtime. 

For  the protocol methods, library users can use MRW to choose what the model should be serialized to: ie `BinaryContent.Create(tree, new ModelReaderWriterOptions ("J"))`</body></comment_new>
<comment_new><author>@JoshLove-msft</author><body>
I don't think a single operation would support both, but if a model supports both does the convenience method apply the correct options when calling the protocol?</body></comment_new>
<comment_new><author>@jorgerangel-msft</author><body>
@JoshLove-msft ah okay, I understand the use case here now. This is something I missed then. If a model is used in an operation that supports json and also an operation that supports xml payloads, the convenience method for one of them would result in a runtime failure since we are using the explicit operator to serialize the payload. That operator is relying on the generated wire format which would be "J". 

I will think about how to fix this. I think maybe having a internal helper on the model's to let us switch betwen json/xml serialization depending on the content type might work, and not use the implicit operator in the convenience method. The deserialization should work with today's implementation though since the operator there checks the response's content type header.</body></comment_new>
</comments>

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@jorgerangel-msft
Copy link
Contributor

@copilot for testing, lets update our sample project spec and remove the @usage decorator from the Tree model https://github.com/microsoft/typespec/blob/main/docs/samples/client/csharp/SampleService/main.tsp#L794. Lets add a new operation to the PlantOperations interface to exericise the scenario where the Tree model is used in both json and xml request payloads:

@route("/plants")
interface PlantOperations {
  @doc("Get a tree as a plant")
  @get
  @route("/tree/as-plant")
  getTree(): {
    @header contentType: "application/xml";
    @body body: Tree;
  };

  @doc("Get a tree as a plant")
  @get
  @route("/tree/as-plant/json")
  getTreeAsJson(): {
    @header contentType: "application/json";
    @body body: Tree;
  };

  @doc("Update a tree as a plant")
  @put
  @route("/tree/as-plant")
  updateTree(@body tree: Tree, @header contentType: "application/xml"): {
    @header contentType: "application/xml";
    @body body: Tree;
  };

  @doc("Update a tree as a plant")
  @put
  @route("/tree/as-plant/json")
  updateTreeAsJson(@body tree: Tree, @header contentType: "application/json"): {
    @header contentType: "application/json";
    @body body: Tree;
  };
}

We should regenerate the project by running ./eng/scripts/Generate.ps1

- Added internal ToBinaryContent(string format) method to models supporting both JSON and XML
- Updated convenience method generation to detect content-type and use appropriate format
- Models with dual format support now call ToBinaryContent("X") for XML or ToBinaryContent("J") for JSON
- Models with single format support continue using implicit operator

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
@microsoft-github-policy-service microsoft-github-policy-service bot added the emitter:client:csharp Issue for the C# client emitter: @typespec/http-client-csharp label Feb 13, 2026
Copilot AI changed the title [WIP] Add logic for XML serialization based on content-type Fix XML serialization in convenience methods for dual-format models Feb 13, 2026
- Removed @Usage decorator from Tree model to get default usage (both JSON and XML)
- Added getTreeAsJson and updateTreeAsJson operations to test JSON serialization
- Verified UpdateTree uses ToBinaryContent("X") for XML
- Verified UpdateTreeAsJson uses ToBinaryContent("J") for JSON

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 13, 2026

@copilot for testing, lets update our sample project spec and remove the @usage decorator from the Tree model https://github.com/microsoft/typespec/blob/main/docs/samples/client/csharp/SampleService...

Done! Changes committed in 29515ad.

The Tree model now demonstrates dual-format support:

  • Removed @usage decorator (gets default JSON+XML support)
  • Added getTreeAsJson and updateTreeAsJson operations for JSON content-type
  • Verified generated code:
    • UpdateTree: calls tree.ToBinaryContent("X") for XML serialization
    • UpdateTreeAsJson: calls tree.ToBinaryContent("J") for JSON serialization

All 1139 generator tests and 838 spector tests pass.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 13, 2026

Open in StackBlitz

npm i https://pkg.pr.new/microsoft/typespec/@typespec/http@9712
npm i https://pkg.pr.new/microsoft/typespec/@typespec/http-client@9712
npm i https://pkg.pr.new/microsoft/typespec/@typespec/http-client-csharp@9712

commit: 43f36ff

- Renamed BuildInternalToBinaryContentMethod to BuildToBinaryContentMethod
- Removed unnecessary local variables (modifiers, mrwOptionsType)
- Simplified parameter description to "The format to use for serialization"
- Used RequestContentType.Name to construct method name (To{RequestContentType.Name})

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
- Added constants for JSON and XML media types
- Changed media type checks to use Contains instead of StartsWith
- Reused bodyModel and bodyInputModel variables instead of recalculating
- Removed null-forgiving operator on requestMediaType
- Used ModelReaderWriterOptionsSnippets for format literals instead of string literals

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
- Removed requestContentType caching in BuildToBinaryContentMethod (only used once)
- Made Invoke call dynamic using To{RequestContentType.Name} pattern for better maintainability

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
- Moved ToBinaryContent logic from GetProtocolMethodArguments to GetStackVariablesForProtocolParamConversion
- Now generates: using BinaryContent content = tree.ToBinaryContent("X");
- Ensures BinaryContent is properly disposed after use
- Updated GetProtocolMethodArguments to use content variable from declarations
- All tests pass and generated code properly disposes resources

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
…e method validation

- Removed unused bodyInputModel variable from GetProtocolMethodArguments (line 606)
- Created TestData files with expected generated code for convenience method tests
- Updated ScmMethodProviderCollectionTests to use Helpers.GetExpectedFromFile() for full method validation
- Tests now validate entire convenience method body instead of just checking for specific strings
- All 1147 ClientModel tests pass

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
- Removed unnecessary reflection when accessing MediaType property
- MediaType is a public property on BinaryContent, so no reflection needed
- Still use reflection to call internal ToBinaryContent method
- Tests pass and code is simpler

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
@azure-sdk
Copy link
Collaborator

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add logic to use XML serialization/deserialization in protocol methods based on the content-type

4 participants