Skip to content

Commit 2ddbc37

Browse files
Propagate tcgc SerializationOptions for body parameters and responses (#10730)
tcgc 0.68.0 added `serializationOptions` to `SdkBodyParameter` and `SdkHttpResponse`, but the C# emitter and MTG were still inferring wire format from the content-type header. This PR plumbs that data through so MTG can consume it directly, and switches the first XML wire-format detection site to use the new metadata. ### Emitter (TypeScript) - Added `serializationOptions: SerializationOptions` (required) to `InputBodyParameter` and `OperationResponse`, matching the existing shape on `InputModelType` / `InputModelProperty`. - Populated the field in `fromBodyParameter` and `fromSdkHttpOperationResponse` by copying straight from tcgc. ### MTG (`Microsoft.TypeSpec.Generator.Input`) - Added nullable `SerializationOptions` (of type `InputSerializationOptions`) to `InputBodyParameter` and `InputOperationResponse`, with constructor parameters defaulted to `null` for back-compat. - Updated `InputBodyParameterConverter` and `InputOperationResponseConverter` to read the new `serializationOptions` JSON field. - Added a new `InputBinarySerializationOptions` type (`IsFile`, `IsText`, `ContentTypes`, `Filename`) and `InputBinarySerializationOptionsConverter`, plumbed through `InputSerializationOptions` / `InputSerializationOptionsConverter` so that binary payload metadata emitted by tcgc on model types can be deserialized in MTG. `Filename` is typed as `InputModelProperty?`, matching `InputMultipartOptions.Filename`. ### MTG (`Microsoft.TypeSpec.Generator.ClientModel`) - `ScmMethodProviderCollection.TryGetXmlCollectionNamesForResponse` now gates on `response.SerializationOptions?.Xml` instead of scanning `response.ContentTypes` for `application/xml`, consuming the propagated metadata directly. - Extended the test-only `InputFactory.OperationResponse` helper with an optional `serializationOptions` parameter and updated the affected XML list response test to supply it. ### Tests - Emitter unit tests in `model-type.test.ts` covering body parameter `serializationOptions` propagation: - `application/json` + `string` body — asserts `bodyParam.serializationOptions.json` is populated. - `Http.File` (`application/octet-stream`) body — asserts the body's model type carries `serializationOptions.binary` with the full set of properties: `isFile === true`, `isText === false`, `contentTypes === ["application/octet-stream"]`, and `filename.name === "filename"`. Also asserts that the body parameter's own `serializationOptions` has neither `json` nor `xml` set (tcgc only derives `json`/`xml` for body params from content types; `binary` lives on the model type). - Added serialization-options deserialization tests directly inside `TypeSpecInputConverterTests` in `Microsoft.TypeSpec.Generator.Input.Tests` validating JSON deserialization of `InputSerializationOptions` (empty, json, xml, binary with all properties including `filename`, binary defaults, unknown property tolerance), `InputBodyParameter.SerializationOptions` (json, binary with `filename`, absent → null), and `InputOperationResponse.SerializationOptions` (xml, binary with `filename`, absent → null). ### Follow-up One header/content-type-based wire-format site remains for a separate change: `MrwSerializationTypeDefinition.Xml.cs` reads the `Content-Type` response header at runtime to branch between `application/json` and `application/xml`; converting that to consume `SerializationOptions` is the natural next step. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
1 parent f8fc11d commit 2ddbc37

91 files changed

Lines changed: 5606 additions & 1226 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/http-client-csharp/emitter/src/lib/operation-converter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,7 @@ function fromBodyParameter(
604604
readOnly: isReadOnly(p),
605605
crossLanguageDefinitionId: p.crossLanguageDefinitionId,
606606
methodParameterSegments: diagnostics.pipe(getMethodParameterSegments(sdkContext, p)),
607+
serializationOptions: p.serializationOptions,
607608
};
608609

609610
sdkContext.__typeCache.updateSdkOperationParameterReferences(p, retVar);
@@ -706,6 +707,7 @@ export function fromSdkHttpOperationResponse(
706707
isErrorResponse:
707708
sdkResponse.type !== undefined && isErrorModel(sdkContext.program, sdkResponse.type.__raw!),
708709
contentTypes: sdkResponse.contentTypes,
710+
serializationOptions: sdkResponse.serializationOptions,
709711
};
710712

711713
sdkContext.__typeCache.updateSdkResponseReferences(sdkResponse, retVar);

packages/http-client-csharp/emitter/src/type/input-type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ export interface InputBodyParameter extends InputPropertyTypeBase {
242242
scope: InputParameterScope;
243243
serializedName: string;
244244
methodParameterSegments?: InputMethodParameter[];
245+
serializationOptions: SerializationOptions;
245246
}
246247

247248
export interface InputEndpointParameter extends InputPropertyTypeBase {

packages/http-client-csharp/emitter/src/type/operation-response.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4+
import { SerializationOptions } from "@azure-tools/typespec-client-generator-core";
45
import { HttpResponseHeader } from "./http-response-header.js";
56
import { InputType } from "./input-type.js";
67

@@ -10,4 +11,5 @@ export interface OperationResponse {
1011
headers: HttpResponseHeader[];
1112
contentTypes?: string[];
1213
isErrorResponse: boolean;
14+
serializationOptions: SerializationOptions;
1315
}

packages/http-client-csharp/emitter/test/Unit/model-type.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { TestHost } from "@typespec/compiler/testing";
55
import assert, { deepStrictEqual, ok, strictEqual } from "assert";
66
import { beforeEach, describe, it, vi } from "vitest";
77
import { createModel } from "../../src/lib/client-model-builder.js";
8+
import { InputModelType } from "../../src/type/input-type.js";
89
import {
910
createCSharpSdkContext,
1011
createEmitterContext,
@@ -1094,4 +1095,72 @@ describe("XML serialization options", () => {
10941095
ok(itemsProperty.serializationOptions.xml.itemsName);
10951096
strictEqual(itemsProperty.serializationOptions.xml.itemsName, "Item");
10961097
});
1098+
1099+
it("Body parameter with file payload should have binary serializationOptions populated on the body type", async function () {
1100+
const program = await typeSpecCompile(
1101+
`
1102+
model RawData extends File {
1103+
contentType: "application/octet-stream";
1104+
contents: bytes;
1105+
}
1106+
1107+
@route("/upload")
1108+
@post
1109+
op uploadRawData(@bodyRoot data: RawData): void;
1110+
`,
1111+
runner,
1112+
{ IsTCGCNeeded: true },
1113+
);
1114+
1115+
const context = createEmitterContext(program);
1116+
const sdkContext = await createCSharpSdkContext(context);
1117+
const [root] = createModel(sdkContext);
1118+
1119+
const method = root.clients[0].methods[0];
1120+
ok(method);
1121+
const bodyParam = method.operation.parameters.find((p) => p.kind === "body");
1122+
ok(bodyParam);
1123+
// The body parameter itself always has serializationOptions (tcgc populates
1124+
// json/xml options from content types; for a binary file body neither is set).
1125+
ok(bodyParam.serializationOptions);
1126+
strictEqual(bodyParam.serializationOptions.json, undefined);
1127+
strictEqual(bodyParam.serializationOptions.xml, undefined);
1128+
// The body's model type carries the binary serialization options.
1129+
const bodyType = bodyParam.type as InputModelType;
1130+
ok(bodyType.serializationOptions);
1131+
ok(bodyType.serializationOptions.binary);
1132+
strictEqual(bodyType.serializationOptions.binary.isFile, true);
1133+
// bytes contents → not text
1134+
strictEqual(bodyType.serializationOptions.binary.isText, false);
1135+
// contentTypes should be populated from the model's contentType property
1136+
ok(bodyType.serializationOptions.binary.contentTypes);
1137+
strictEqual(bodyType.serializationOptions.binary.contentTypes.length, 1);
1138+
strictEqual(bodyType.serializationOptions.binary.contentTypes[0], "application/octet-stream");
1139+
// filename should be populated for an Http.File-derived model
1140+
ok(bodyType.serializationOptions.binary.filename);
1141+
strictEqual(bodyType.serializationOptions.binary.filename.name, "filename");
1142+
});
1143+
1144+
it("Body parameter with JSON content type should have json serializationOptions populated", async function () {
1145+
const program = await typeSpecCompile(
1146+
`
1147+
@route("/messages")
1148+
@post
1149+
op sendMessage(@header contentType: "application/json", @body message: string): void;
1150+
`,
1151+
runner,
1152+
{ IsTCGCNeeded: true },
1153+
);
1154+
1155+
const context = createEmitterContext(program);
1156+
const sdkContext = await createCSharpSdkContext(context);
1157+
const [root] = createModel(sdkContext);
1158+
1159+
const method = root.clients[0].methods[0];
1160+
ok(method);
1161+
const bodyParam = method.operation.parameters.find((p) => p.kind === "body");
1162+
ok(bodyParam);
1163+
ok(bodyParam.serializationOptions);
1164+
ok(bodyParam.serializationOptions.json);
1165+
});
10971166
});

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,9 +1380,9 @@ private bool TryGetXmlCollectionNamesForResponse(
13801380
rootName = null;
13811381
childName = null;
13821382

1383-
// Check if the response uses XML content type
1383+
// Check if the response is serialized as XML using the propagated serialization options
13841384
var response = ServiceMethod.Operation.Responses.FirstOrDefault(r => !r.IsErrorResponse);
1385-
if (response == null || !response.ContentTypes.Any(c => c.Contains(XmlMediaType, StringComparison.OrdinalIgnoreCase)))
1385+
if (response?.SerializationOptions?.Xml is null)
13861386
{
13871387
return false;
13881388
}

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1924,7 +1924,12 @@ public void ConvenienceMethod_XmlListResponse_UsesXDocumentDeserialization()
19241924
var operation = InputFactory.Operation(
19251925
"GetFoo",
19261926
httpMethod: "GET",
1927-
responses: [InputFactory.OperationResponse([200], bodytype: arrayType, contentTypes: ["application/xml"])]);
1927+
responses: [InputFactory.OperationResponse(
1928+
[200],
1929+
bodytype: arrayType,
1930+
contentTypes: ["application/xml"],
1931+
serializationOptions: InputFactory.Serialization.Options(
1932+
xml: InputFactory.Serialization.Xml("SignedIdentifier")))]);
19281933

19291934
var serviceMethod = InputFactory.BasicServiceMethod(
19301935
"GetFoo",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Generic;
5+
6+
namespace Microsoft.TypeSpec.Generator.Input
7+
{
8+
/// <summary>
9+
/// Describes how a body is serialized as a binary payload (e.g. a file or raw stream).
10+
/// </summary>
11+
public class InputBinarySerializationOptions
12+
{
13+
public InputBinarySerializationOptions(bool isFile = false, bool? isText = null, IReadOnlyList<string>? contentTypes = null, InputModelProperty? filename = null)
14+
{
15+
IsFile = isFile;
16+
IsText = isText;
17+
ContentTypes = contentTypes;
18+
Filename = filename;
19+
}
20+
21+
/// <summary>
22+
/// Whether this is a file/stream input.
23+
/// </summary>
24+
public bool IsFile { get; internal set; }
25+
26+
/// <summary>
27+
/// Whether the file contents should be represented as a string or a raw byte stream.
28+
/// Only set when <see cref="IsFile"/> is <c>true</c>.
29+
/// </summary>
30+
public bool? IsText { get; internal set; }
31+
32+
/// <summary>
33+
/// The list of inner media types of the file.
34+
/// Only set when <see cref="IsFile"/> is <c>true</c>.
35+
/// </summary>
36+
public IReadOnlyList<string>? ContentTypes { get; internal set; }
37+
38+
/// <summary>
39+
/// The model property that represents the filename in the file model.
40+
/// Only set when <see cref="IsFile"/> is <c>true</c>.
41+
/// </summary>
42+
public InputModelProperty? Filename { get; internal set; }
43+
}
44+
}

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputBodyParameter.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,21 @@ public InputBodyParameter(
2020
InputConstant? defaultValue,
2121
InputParameterScope scope,
2222
IReadOnlyList<string> contentTypes,
23-
string defaultContentType)
23+
string defaultContentType,
24+
InputSerializationOptions? serializationOptions = null)
2425
: base(name, summary, doc, type, isRequired, isReadOnly, access, serializedName, isApiVersion, defaultValue, scope)
2526
{
2627
ContentTypes = contentTypes;
2728
DefaultContentType = defaultContentType;
29+
SerializationOptions = serializationOptions;
2830
}
2931

3032
public IReadOnlyList<string> ContentTypes { get; internal set; }
3133
public string DefaultContentType { get; internal set; }
34+
35+
/// <summary>
36+
/// Options describing how the body is serialized on the wire.
37+
/// </summary>
38+
public InputSerializationOptions? SerializationOptions { get; internal set; }
3239
}
3340
}

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputOperationResponse.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ namespace Microsoft.TypeSpec.Generator.Input
1111
/// </summary>
1212
public sealed class InputOperationResponse
1313
{
14-
public InputOperationResponse(IReadOnlyList<int> statusCodes, InputType? bodyType, IReadOnlyList<InputOperationResponseHeader> headers, bool isErrorResponse, IReadOnlyList<string> contentTypes)
14+
public InputOperationResponse(IReadOnlyList<int> statusCodes, InputType? bodyType, IReadOnlyList<InputOperationResponseHeader> headers, bool isErrorResponse, IReadOnlyList<string> contentTypes, InputSerializationOptions? serializationOptions = null)
1515
{
1616
StatusCodes = statusCodes;
1717
BodyType = bodyType;
1818
Headers = headers;
1919
IsErrorResponse = isErrorResponse;
2020
ContentTypes = contentTypes;
21+
SerializationOptions = serializationOptions;
2122
}
2223

2324
public InputOperationResponse() : this(Array.Empty<int>(), null, Array.Empty<InputOperationResponseHeader>(), false, Array.Empty<string>()) { }
@@ -27,5 +28,10 @@ public InputOperationResponse() : this(Array.Empty<int>(), null, Array.Empty<In
2728
public IReadOnlyList<InputOperationResponseHeader> Headers { get; }
2829
public bool IsErrorResponse { get; }
2930
public IReadOnlyList<string> ContentTypes { get; }
31+
32+
/// <summary>
33+
/// Options describing how the response body is deserialized from the wire.
34+
/// </summary>
35+
public InputSerializationOptions? SerializationOptions { get; }
3036
}
3137
}

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputSerializationOptions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@ namespace Microsoft.TypeSpec.Generator.Input
55
{
66
public class InputSerializationOptions
77
{
8-
public InputSerializationOptions(InputJsonSerializationOptions? json = null, InputXmlSerializationOptions? xml = null, InputMultipartOptions? multipart = null)
8+
public InputSerializationOptions(InputJsonSerializationOptions? json = null, InputXmlSerializationOptions? xml = null, InputMultipartOptions? multipart = null, InputBinarySerializationOptions? binary = null)
99
{
1010
Json = json;
1111
Xml = xml;
1212
Multipart = multipart;
13+
Binary = binary;
1314
}
1415

1516
public InputJsonSerializationOptions? Json { get; internal set; }
1617

1718
public InputXmlSerializationOptions? Xml { get; internal set; }
1819

1920
public InputMultipartOptions? Multipart { get; internal set; }
21+
22+
public InputBinarySerializationOptions? Binary { get; internal set; }
2023
}
2124
}

0 commit comments

Comments
 (0)