From bb715f44d66e29fdeccd8406e4283ea75996b6b1 Mon Sep 17 00:00:00 2001 From: "Jiao Di (MSFT)" Date: Fri, 12 Dec 2025 16:03:22 +0800 Subject: [PATCH 1/2] add ut --- .../modularUnit/scenarios/models/fileModel.md | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 packages/typespec-ts/test/modularUnit/scenarios/models/fileModel.md diff --git a/packages/typespec-ts/test/modularUnit/scenarios/models/fileModel.md b/packages/typespec-ts/test/modularUnit/scenarios/models/fileModel.md new file mode 100644 index 0000000000..3c889a1890 --- /dev/null +++ b/packages/typespec-ts/test/modularUnit/scenarios/models/fileModel.md @@ -0,0 +1,78 @@ +# only: Should generate file model + +## TypeSpec + +```tsp +model Site { + name: string; +} +op test(@body site:Site): File<"application/octet-stream">; +``` + +## Model + +```ts models +/** + * This file contains only generated model types and their (de)serializers. + * Disable the following rules for internal models with '_' prefix and deserializers which require 'any' for raw JSON input. + */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/** model interface Site */ +export interface Site { + name: string; +} + +export function siteSerializer(item: Site): any { + return { name: item["name"] }; +} +``` + +## Operations + +```ts operations +import { TestingContext as Client } from "./index.js"; +import { Site, siteSerializer } from "../models/models.js"; +import { TestOptionalParams } from "./options.js"; +import { + StreamableMethod, + PathUncheckedResponse, + createRestError, + operationOptionsToRequestParameters, +} from "@azure-rest/core-client"; + +export function _testSend( + context: Client, + site: Site, + options: TestOptionalParams = { requestOptions: {} }, +): StreamableMethod { + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + contentType: "application/json", + headers: { accept: "application/octet-stream", ...options.requestOptions?.headers }, + body: siteSerializer(site), + }); +} + +export async function _testDeserialize( + result: PathUncheckedResponse, +): Promise<__PLACEHOLDER_o22__> { + const expectedStatuses = ["200"]; + if (!expectedStatuses.includes(result.status)) { + throw createRestError(result); + } + + return __PLACEHOLDER_o22_sdeserializer__(result.body); +} + +export async function test( + context: Client, + site: Site, + options: TestOptionalParams = { requestOptions: {} }, +): Promise<__PLACEHOLDER_o22__> { + const result = await _testSend(context, site, options); + return _testDeserialize(result); +} +``` \ No newline at end of file From eed23ac505a5e886321f2fb65e4a85883dcee3f6 Mon Sep 17 00:00:00 2001 From: "Jiao Di (MSFT)" Date: Wed, 21 Jan 2026 16:59:49 +0800 Subject: [PATCH 2/2] fix --- .../src/modular/helpers/operationHelpers.ts | 64 ++++++++++++------- .../buildDeserializerFunction.ts | 7 ++ .../type-expressions/get-type-expression.ts | 4 ++ .../modularUnit/scenarios/models/fileModel.md | 14 ++-- 4 files changed, 60 insertions(+), 29 deletions(-) diff --git a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts index cef645c5ca..06e4b9bd2c 100644 --- a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts +++ b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts @@ -232,30 +232,42 @@ export function getDeserializePrivateFunction( if (deserializedType) { const contentTypes = operation.operation.responses[0]?.contentTypes; - const deserializeFunctionName = buildModelDeserializer( - context, - deserializedType, - { - nameOnly: true, - skipDiscriminatedUnionSuffix: false - } - ); - if (deserializeFunctionName) { - statements.push(`return ${deserializeFunctionName}(${deserializedRoot})`); - } else if (isAzureCoreErrorType(context.program, deserializedType.__raw)) { + // For File type responses, directly return the body + if ( + deserializedType.kind === "model" && + deserializedType.crossLanguageDefinitionId === "TypeSpec.Http.File" + ) { statements.push(`return ${deserializedRoot}`); } else { - statements.push( - `return ${deserializeResponseValue( - context, - deserializedType, - deserializedRoot, - true, - isBinaryPayload(context, response.type!.__raw!, contentTypes!) - ? "binary" - : getEncodeForType(deserializedType) - )}` + const deserializeFunctionName = buildModelDeserializer( + context, + deserializedType, + { + nameOnly: true, + skipDiscriminatedUnionSuffix: false + } ); + if (deserializeFunctionName) { + statements.push( + `return ${deserializeFunctionName}(${deserializedRoot})` + ); + } else if ( + isAzureCoreErrorType(context.program, deserializedType.__raw) + ) { + statements.push(`return ${deserializedRoot}`); + } else { + statements.push( + `return ${deserializeResponseValue( + context, + deserializedType, + deserializedRoot, + true, + isBinaryPayload(context, response.type!.__raw!, contentTypes!) + ? "binary" + : getEncodeForType(deserializedType) + )}` + ); + } } } else if (returnType.type === "void") { statements.push("return;"); @@ -501,7 +513,11 @@ export function getOperationFunction( const parameterList = parameters.map((p) => p.name).join(", "); // Special case for binary-only bodies: use helper to call streaming methods so that Core doesn't poison the response body by // doing a UTF-8 decode on the raw bytes. - if (response?.type?.kind === "bytes" && response.type.encode === "bytes") { + const isBinaryResponse = + (response?.type?.kind === "bytes" && response.type.encode === "bytes") || + (response?.type?.kind === "model" && + response.type.crossLanguageDefinitionId === "TypeSpec.Http.File"); + if (isBinaryResponse) { statements.push(`const streamableMethod = _${name}Send(${parameterList});`); statements.push( `const result = await ${resolveReference(SerializationHelpers.getBinaryResponse)}(streamableMethod);` @@ -1685,6 +1701,10 @@ export function deserializeResponseValue( return `${restValue} as any`; } case "model": // generate deserialize logic for spread model types + // Special handling for TypeSpec.Http.File - it's already a Uint8Array + if (type.crossLanguageDefinitionId === "TypeSpec.Http.File") { + return restValue; + } return `{${getResponseMapping(context, type, "").join(",")}}`; case "nullable": return deserializeResponseValue( diff --git a/packages/typespec-ts/src/modular/serialization/buildDeserializerFunction.ts b/packages/typespec-ts/src/modular/serialization/buildDeserializerFunction.ts index f78ccebd84..0bf6d94915 100644 --- a/packages/typespec-ts/src/modular/serialization/buildDeserializerFunction.ts +++ b/packages/typespec-ts/src/modular/serialization/buildDeserializerFunction.ts @@ -80,6 +80,13 @@ export function buildModelDeserializer( } ): FunctionDeclarationStructure | undefined | string { // const modelTcgcType = getTcgcType(type) as SdkModelType; + // TypeSpec.Http.File doesn't need deserialization - it's already a Uint8Array + if ( + type.kind === "model" && + type.crossLanguageDefinitionId === "TypeSpec.Http.File" + ) { + return undefined; + } if (!isSupportedSerializeType(type)) { return undefined; } diff --git a/packages/typespec-ts/src/modular/type-expressions/get-type-expression.ts b/packages/typespec-ts/src/modular/type-expressions/get-type-expression.ts index ca8c409611..6ce27ffa71 100644 --- a/packages/typespec-ts/src/modular/type-expressions/get-type-expression.ts +++ b/packages/typespec-ts/src/modular/type-expressions/get-type-expression.ts @@ -89,6 +89,10 @@ export function getTypeExpression( return `Record`; } case "model": + // Special handling for TypeSpec.Http.File + if (type.crossLanguageDefinitionId === "TypeSpec.Http.File") { + return "Uint8Array"; + } return getModelExpression(context, type); case "nullable": return getNullableExpression(context, type, options); diff --git a/packages/typespec-ts/test/modularUnit/scenarios/models/fileModel.md b/packages/typespec-ts/test/modularUnit/scenarios/models/fileModel.md index 3c889a1890..2aa0253d62 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/models/fileModel.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/models/fileModel.md @@ -1,4 +1,4 @@ -# only: Should generate file model +# Should generate file model ## TypeSpec @@ -33,6 +33,7 @@ export function siteSerializer(item: Site): any { ```ts operations import { TestingContext as Client } from "./index.js"; import { Site, siteSerializer } from "../models/models.js"; +import { getBinaryResponse } from "../static-helpers/serialization/get-binary-response.js"; import { TestOptionalParams } from "./options.js"; import { StreamableMethod, @@ -56,23 +57,22 @@ export function _testSend( }); } -export async function _testDeserialize( - result: PathUncheckedResponse, -): Promise<__PLACEHOLDER_o22__> { +export async function _testDeserialize(result: PathUncheckedResponse): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { throw createRestError(result); } - return __PLACEHOLDER_o22_sdeserializer__(result.body); + return result.body; } export async function test( context: Client, site: Site, options: TestOptionalParams = { requestOptions: {} }, -): Promise<__PLACEHOLDER_o22__> { - const result = await _testSend(context, site, options); +): Promise { + const streamableMethod = _testSend(context, site, options); + const result = await getBinaryResponse(streamableMethod); return _testDeserialize(result); } ``` \ No newline at end of file