Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -418,15 +418,23 @@ function buildModelTypeSerializer(
statements: ["return item;"]
};

// Check if this is truly multipart/form-data by verifying that properties have multipart serialization options
// This prevents multipart/mixed (and other non-form-data multipart types) from being incorrectly serialized as form data
const properties = getAllProperties(context, type, getAllAncestors(type));
const hasMultipartProperties = properties.some(
(prop) => prop.kind === "property" && prop.serializationOptions?.multipart
);

if (
(type.usage & UsageFlags.Input) === UsageFlags.Input &&
(type.usage & UsageFlags.MultipartFormData) === UsageFlags.MultipartFormData
(type.usage & UsageFlags.MultipartFormData) ===
UsageFlags.MultipartFormData &&
hasMultipartProperties
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot add a test case with multipart/mixed to verify the new behavior

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test case in test/modularUnit/scenarios/multipart/plainBody.md that verifies models without HttpPart<T> properties use default serialization instead of array-of-parts serialization. The test demonstrates that plain body models generate { body: item["body"] } rather than [createFilePartDescriptor(...)]. Commit: 29ade60

) {
// For MFD models, serialize into an array of parts
// TODO: cleaner abstraction, quite a bit of duplication with the non-MFD stuff here
const parts: string[] = [];

const properties = getAllProperties(context, type, getAllAncestors(type));
for (const property of properties) {
if (property.kind !== "property") {
continue;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Plain body without HttpPart should use default serialization

When a model is used with multipart content type but properties don't use `HttpPart<T>`, the serializer should use default handling (return the item as-is) rather than array-of-parts serialization.

This scenario represents cases like Azure Storage's `submitBatch` where the entire multipart content is assembled as a plain string body.

## TypeSpec

```tsp
model BatchRequest {
body: string;
}

@route("/batch")
op submitBatch(@body body: BatchRequest): void;
```

## Models

For a model without HttpPart properties, even if it's used with multipart, the serializer should just return the item unchanged:

```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 BatchRequest */
export interface BatchRequest {
body: string;
}

export function batchRequestSerializer(item: BatchRequest): any {
return { body: item["body"] };
}
```

## Operations

```ts operations
import { TestingContext as Client } from "./index.js";
import { BatchRequest, batchRequestSerializer } from "../models/models.js";
import { SubmitBatchOptionalParams } from "./options.js";
import {
StreamableMethod,
PathUncheckedResponse,
createRestError,
operationOptionsToRequestParameters,
} from "@azure-rest/core-client";

export function _submitBatchSend(
context: Client,
body: BatchRequest,
options: SubmitBatchOptionalParams = { requestOptions: {} },
): StreamableMethod {
return context
.path("/batch")
.post({
...operationOptionsToRequestParameters(options),
contentType: "application/json",
body: batchRequestSerializer(body),
});
}

export async function _submitBatchDeserialize(result: PathUncheckedResponse): Promise<void> {
const expectedStatuses = ["204"];
if (!expectedStatuses.includes(result.status)) {
throw createRestError(result);
}

return;
}

export async function submitBatch(
context: Client,
body: BatchRequest,
options: SubmitBatchOptionalParams = { requestOptions: {} },
): Promise<void> {
const result = await _submitBatchSend(context, body, options);
return _submitBatchDeserialize(result);
}
```
Loading