From 64682b3b88a3f99c7c1906d1ad8f3470a619d6c4 Mon Sep 17 00:00:00 2001 From: "Jiao Di (MSFT)" Date: Tue, 3 Feb 2026 17:02:13 +0800 Subject: [PATCH 01/10] fix --- .../src/modular/helpers/operationHelpers.ts | 28 ++++++++++++++++-- .../serialization/buildSerializerFunction.ts | 23 +++++++++++++-- .../flatten-property/src/index.d.ts | 29 +++++++++++++++++++ .../modelFlatten.spec.ts | 19 ++++++++++++ .../serialization/readonlyFlattenModel.md | 17 ++++------- 5 files changed, 99 insertions(+), 17 deletions(-) diff --git a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts index 0f914fe19d..c0385a27a9 100644 --- a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts +++ b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts @@ -1514,6 +1514,12 @@ function getSerializationExpressionForFlatten( !isReadOnly(p) && !isMetadata(context.program, p.__raw!) ); + + // If all properties are readonly, don't serialize this flatten property at all + if (validProps.length === 0) { + return "undefined"; + } + const optionalPrefix = property.optional ? `${resolveReference(SerializationHelpers.areAllPropsUndefined)}(${propertyPath}, [${validProps .map((p) => `"${p.name}"`) @@ -1618,7 +1624,9 @@ export function getRequestModelMapping( propertyPath, overrides, enableFlatten - ).map(([name, value]) => `"${name}": ${value}`); + ) + .filter(([_name, value]) => value !== "undefined") + .map(([name, value]) => `"${name}": ${value}`); } function getPropertySerializedName( @@ -1684,8 +1692,24 @@ export function getResponseMapping( const propertyName = normalizeModelPropertyName(context, property); if (deserializeFunctionName) { if (isSupportedFlatten) { + // Check if all properties of the flattened type are readonly + const flattenedProps = getAllProperties( + context, + property.type, + getAllAncestors(property.type) + ).filter( + (p) => p.kind === "property" && !isMetadata(context.program, p.__raw!) + ); + const allPropsReadonly = flattenedProps.every((p) => isReadOnly(p)); + + // For flatten properties in responses: + // - If all properties are readonly, they're flattened at the parent level + // - Otherwise, they're nested under the property name + const flattenPath = allPropsReadonly + ? propertyPath || "item" + : restValue; props.push( - `...${nullOrUndefinedPrefix}${deserializeFunctionName}(${restValue})` + `...${nullOrUndefinedPrefix}${deserializeFunctionName}(${flattenPath})` ); } else { props.push( diff --git a/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts b/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts index 79eae42f16..4c0ddeac7c 100644 --- a/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts +++ b/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts @@ -507,9 +507,26 @@ function buildModelTypeSerializer( return ${serializeContent} `); } else { - output.push(` - return item; - `); + // For flatten properties, if all properties are readonly, return empty object + // Otherwise, return the item itself + if (options.flatten) { + // Change parameter name to _item to indicate it's intentionally unused + serializerFunction.parameters = [ + { + name: "_item", + type: options.flatten + ? resolveReference(refkey(options.flatten.baseModel)) + : resolveReference(refkey(type)) + } + ]; + output.push(` + return {}; + `); + } else { + output.push(` + return item; + `); + } } serializerFunction.statements = output; } diff --git a/packages/typespec-ts/test/azureModularIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts b/packages/typespec-ts/test/azureModularIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts index 64fd799fc6..7af3981259 100644 --- a/packages/typespec-ts/test/azureModularIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts +++ b/packages/typespec-ts/test/azureModularIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts @@ -23,6 +23,8 @@ export declare class FlattenPropertyClient { private _client; readonly pipeline: Pipeline; constructor(options?: FlattenPropertyClientOptionalParams); + putFlattenReadOnlyModel(body: Solution, options?: PutFlattenReadOnlyModelOptionalParams): Promise; + putFlattenUnknownModel(input: FlattenUnknownModel, options?: PutFlattenUnknownModelOptionalParams): Promise; putNestedFlattenModel(input: NestedFlattenModel, options?: PutNestedFlattenModelOptionalParams): Promise; putFlattenModel(input: FlattenModel, options?: PutFlattenModelOptionalParams): Promise; } @@ -30,6 +32,11 @@ export declare class FlattenPropertyClient { export declare interface FlattenPropertyClientOptionalParams extends ClientOptions { } +export declare interface FlattenUnknownModel { + name: string; + properties?: any; +} + export declare interface NestedFlattenModel { name: string; summary: string; @@ -39,7 +46,29 @@ export declare interface NestedFlattenModel { export declare interface PutFlattenModelOptionalParams extends OperationOptions { } +export declare interface PutFlattenReadOnlyModelOptionalParams extends OperationOptions { +} + +export declare interface PutFlattenUnknownModelOptionalParams extends OperationOptions { +} + export declare interface PutNestedFlattenModelOptionalParams extends OperationOptions { } +export declare interface Solution { + name: string; + readonly solutionId?: string; + readonly title?: string; + readonly content?: string; + readonly solutionIdPropertiesOptionalSolutionId?: string; + readonly titlePropertiesOptionalTitle?: string; + readonly contentPropertiesOptionalContent?: string; +} + +export declare interface SolutionProperties { + readonly solutionId?: string; + readonly title?: string; + readonly content?: string; +} + export { } diff --git a/packages/typespec-ts/test/azureModularIntegration/modelFlatten.spec.ts b/packages/typespec-ts/test/azureModularIntegration/modelFlatten.spec.ts index 81c01ab753..69b93bfb53 100644 --- a/packages/typespec-ts/test/azureModularIntegration/modelFlatten.spec.ts +++ b/packages/typespec-ts/test/azureModularIntegration/modelFlatten.spec.ts @@ -35,4 +35,23 @@ describe("Property Flatten Client", () => { assert.strictEqual(result.properties.description, "foo"); assert.strictEqual(result.properties.age, 1); }); + + it("Update and receive model with unknown properties flattening", async () => { + const result = await client.putFlattenUnknownModel({ + name: "foo" + }); + assert.strictEqual(result.name, "test"); + assert.strictEqual(result.properties?.key1, "value1"); + assert.strictEqual(result.properties?.key2, "value2"); + }); + + it("Update and receive model with read-only properties flattening", async () => { + const result = await client.putFlattenReadOnlyModel({ + name: "foo" + }); + assert.strictEqual(result.name, "foo"); + assert.strictEqual(result.solutionId, "solution1"); + assert.strictEqual(result.title, "Solution Title"); + assert.strictEqual(result.content, "Solution Content"); + }); }); diff --git a/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md b/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md index 1c0f541a59..624af97b15 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md @@ -35,8 +35,6 @@ needTCGC: true ## Models ```ts models -import { areAllPropsUndefined } from "../static-helpers/serialization/check-prop-undefined.js"; - /** * 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. @@ -54,12 +52,7 @@ export interface Solution { } export function solutionSerializer(item: Solution): any { - return { - properties: _solutionPropertiesSerializer(item), - propertiesOptional: areAllPropsUndefined(item, []) - ? undefined - : _solutionPropertiesOptionalSerializer(item), - }; + return item; } /** model interface SolutionProperties */ @@ -73,11 +66,11 @@ export function solutionPropertiesSerializer(item: SolutionProperties): any { return item; } -export function _solutionPropertiesSerializer(item: Solution): any { - return item; +export function _solutionPropertiesSerializer(_item: Solution): any { + return {}; } -export function _solutionPropertiesOptionalSerializer(item: Solution): any { - return item; +export function _solutionPropertiesOptionalSerializer(_item: Solution): any { + return {}; } ``` From 173244b7a7155252af758841c7e3027a9863a8a3 Mon Sep 17 00:00:00 2001 From: "Jiao Di (MSFT)" Date: Thu, 26 Feb 2026 14:04:06 +0800 Subject: [PATCH 02/10] update case --- packages/typespec-ts/package.json | 2 +- .../flatten-property/src/index.d.ts | 63 +++++++++++++++++++ .../azureIntegration/modelFlatten.spec.ts | 33 ++++++++++ pnpm-lock.yaml | 10 +-- 4 files changed, 102 insertions(+), 6 deletions(-) diff --git a/packages/typespec-ts/package.json b/packages/typespec-ts/package.json index 2b142f057e..ab3738b788 100644 --- a/packages/typespec-ts/package.json +++ b/packages/typespec-ts/package.json @@ -69,7 +69,7 @@ "@typespec/spector": "0.1.0-alpha.24-dev.0", "@typespec/spec-api": "0.1.0-alpha.13-dev.0", "@typespec/tspd": "0.74.0", - "@azure-tools/azure-http-specs": "0.1.0-alpha.38-dev.2", + "@azure-tools/azure-http-specs": "0.1.0-alpha.38-dev.4", "@azure-tools/typespec-autorest": "^0.65.0", "@azure-tools/typespec-azure-core": "^0.65.0", "@azure-tools/typespec-azure-resource-manager": "^0.65.0", diff --git a/packages/typespec-ts/test/azureIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts b/packages/typespec-ts/test/azureIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts index d99d60a3c2..17804325f4 100644 --- a/packages/typespec-ts/test/azureIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts +++ b/packages/typespec-ts/test/azureIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts @@ -44,6 +44,16 @@ export declare type FlattenPropertyClient = Client & { export declare interface FlattenPropertyClientOptions extends ClientOptions { } +export declare interface FlattenUnknownModel { + name: string; + properties?: unknown; +} + +export declare interface FlattenUnknownModelOutput { + name: string; + properties?: any; +} + export declare interface NestedFlattenModel { name: string; properties: ChildFlattenModel; @@ -69,6 +79,36 @@ export declare interface PutFlattenModelBodyParam { export declare type PutFlattenModelParameters = PutFlattenModelBodyParam & RequestParameters; +export declare interface PutFlattenReadOnlyModel { + put(options: PutFlattenReadOnlyModelParameters): StreamableMethod; +} + +export declare interface PutFlattenReadOnlyModel200Response extends HttpResponse { + status: "200"; + body: SolutionOutput; +} + +export declare interface PutFlattenReadOnlyModelBodyParam { + body: Solution; +} + +export declare type PutFlattenReadOnlyModelParameters = PutFlattenReadOnlyModelBodyParam & RequestParameters; + +export declare interface PutFlattenUnknownModel { + put(options: PutFlattenUnknownModelParameters): StreamableMethod; +} + +export declare interface PutFlattenUnknownModel200Response extends HttpResponse { + status: "200"; + body: FlattenUnknownModelOutput; +} + +export declare interface PutFlattenUnknownModelBodyParam { + body: FlattenUnknownModel; +} + +export declare type PutFlattenUnknownModelParameters = PutFlattenUnknownModelBodyParam & RequestParameters; + export declare interface PutNestedFlattenModel { put(options: PutNestedFlattenModelParameters): StreamableMethod; } @@ -87,6 +127,29 @@ export declare type PutNestedFlattenModelParameters = PutNestedFlattenModelBodyP export declare interface Routes { (path: "/azure/client-generator-core/flatten-property/flattenModel"): PutFlattenModel; (path: "/azure/client-generator-core/flatten-property/nestedFlattenModel"): PutNestedFlattenModel; + (path: "/azure/client-generator-core/flatten-property/flattenUnknownModel"): PutFlattenUnknownModel; + (path: "/azure/client-generator-core/flatten-property/flattenReadOnlyModel"): PutFlattenReadOnlyModel; +} + +export declare interface Solution { + name: string; + properties: SolutionProperties; + propertiesOptional?: SolutionProperties; +} + +export declare interface SolutionOutput { + name: string; + properties: SolutionPropertiesOutput; + propertiesOptional?: SolutionPropertiesOutput; +} + +export declare interface SolutionProperties { +} + +export declare interface SolutionPropertiesOutput { + readonly solutionId?: string; + readonly title?: string; + readonly content?: string; } export { } diff --git a/packages/typespec-ts/test/azureIntegration/modelFlatten.spec.ts b/packages/typespec-ts/test/azureIntegration/modelFlatten.spec.ts index 2c9fda5a33..b69aedd8f5 100644 --- a/packages/typespec-ts/test/azureIntegration/modelFlatten.spec.ts +++ b/packages/typespec-ts/test/azureIntegration/modelFlatten.spec.ts @@ -50,4 +50,37 @@ describe("Flatten Property Rest Client", () => { assert.strictEqual(result.body.properties.properties.description, "foo"); assert.strictEqual(result.body.properties.properties.age, 1); }); + + it("should update and receive model with unknown flatten property", async () => { + const result = await client + .path("/azure/client-generator-core/flatten-property/flattenUnknownModel") + .put({ + body: { + name: "foo" + } + }); + assert.strictEqual(result.status, "200"); + assert.strictEqual(result.body.name, "test"); + assert.deepEqual(result.body.properties, { + key1: "value1", + key2: "value2" + }); + }); + + it("should update and receive model with all readonly flatten properties", async () => { + const result = await client + .path( + "/azure/client-generator-core/flatten-property/flattenReadOnlyModel" + ) + .put({ + body: { + name: "foo" + } as any + }); + assert.strictEqual(result.status, "200"); + assert.strictEqual(result.body.name, "foo"); + assert.strictEqual((result.body as any).solutionId, "solution1"); + assert.strictEqual((result.body as any).title, "Solution Title"); + assert.strictEqual((result.body as any).content, "Solution Content"); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1394e073b6..9bf7446778 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -361,8 +361,8 @@ importers: specifier: ^2.3.1 version: 2.5.1 '@azure-tools/azure-http-specs': - specifier: 0.1.0-alpha.38-dev.2 - version: 0.1.0-alpha.38-dev.2(d5a39e74174d9ff5d871e042fc1520d9) + specifier: 0.1.0-alpha.38-dev.4 + version: 0.1.0-alpha.38-dev.4(d5a39e74174d9ff5d871e042fc1520d9) '@azure-tools/typespec-autorest': specifier: ^0.65.0 version: 0.65.0(7d4c1aac8af46b0454a1d8783bae6311) @@ -531,8 +531,8 @@ packages: resolution: {integrity: sha512-X1C7XdyCuo50ch9FzKtTvmK18FgDxxf1Bbt3cSoknQqeDaRegHSSCO+zByq2YA4NvUzKXeZ1engh29IDxZXgpQ==} engines: {node: '>=10.12.0'} - '@azure-tools/azure-http-specs@0.1.0-alpha.38-dev.2': - resolution: {integrity: sha512-rrxSHv70c5d9ZZQRN1xZ849/vzAFM7uHEkpjSNtKBBkx/6/2J2arSiltk4OP6xE/lqHPVPeANzbYQsL4b4wNZw==} + '@azure-tools/azure-http-specs@0.1.0-alpha.38-dev.4': + resolution: {integrity: sha512-RcMWlJoH/ZfFXR4X/nZ/L6siNDtYylYERikcF23JbJ1/SrYsFIS3IpdG4E1YlEMwwfgolL31CDM32+d6g+AViQ==} engines: {node: '>=20.0.0'} peerDependencies: '@azure-tools/typespec-azure-core': ^0.65.0 || >=0.66.0-dev <0.66.0 @@ -5502,7 +5502,7 @@ snapshots: '@azure-tools/tasks': 3.0.255 proper-lockfile: 2.0.1 - '@azure-tools/azure-http-specs@0.1.0-alpha.38-dev.2(d5a39e74174d9ff5d871e042fc1520d9)': + '@azure-tools/azure-http-specs@0.1.0-alpha.38-dev.4(d5a39e74174d9ff5d871e042fc1520d9)': dependencies: '@azure-tools/typespec-azure-core': 0.65.0(@typespec/compiler@1.9.0(@types/node@25.0.8))(@typespec/http@1.9.0(@typespec/compiler@1.9.0(@types/node@25.0.8))(@typespec/streams@0.77.0(@typespec/compiler@1.9.0(@types/node@25.0.8))))(@typespec/rest@0.79.0(@typespec/compiler@1.9.0(@types/node@25.0.8))(@typespec/http@1.9.0(@typespec/compiler@1.9.0(@types/node@25.0.8))(@typespec/streams@0.77.0(@typespec/compiler@1.9.0(@types/node@25.0.8))))) '@typespec/compiler': 1.9.0(@types/node@25.0.8) From 56e36db7d5838013f4ede86cced9f77231b2f4ed Mon Sep 17 00:00:00 2001 From: "Jiao Di (MSFT)" Date: Thu, 5 Mar 2026 15:50:32 +0800 Subject: [PATCH 03/10] update --- .../src/modular/helpers/operationHelpers.ts | 29 ++++--------------- .../serialization/readonlyFlattenModel.md | 2 +- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts index 455e4d9c84..4221a54e5f 100644 --- a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts +++ b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts @@ -1951,12 +1951,11 @@ function getSerializationExpressionForFlatten( !isReadOnly(p) && !isMetadata(context.program, p.__raw!) ); - - // If all properties are readonly, don't serialize this flatten property at all + // If all properties in the flattened model are read-only, omit the field entirely + // so it is not included in the serialized request body. if (validProps.length === 0) { - return "undefined"; + return `undefined`; } - const optionalPrefix = property.optional ? `${resolveReference(SerializationHelpers.areAllPropsUndefined)}(${propertyPath}, [${validProps .map((p) => `"${p.name}"`) @@ -2061,9 +2060,7 @@ export function getRequestModelMapping( propertyPath, overrides, enableFlatten - ) - .filter(([_name, value]) => value !== "undefined") - .map(([name, value]) => `"${name}": ${value}`); + ).map(([name, value]) => `"${name}": ${value}`); } export function getPropertySerializedName( @@ -2129,24 +2126,8 @@ export function getResponseMapping( const propertyName = normalizeModelPropertyName(context, property); if (deserializeFunctionName) { if (isSupportedFlatten) { - // Check if all properties of the flattened type are readonly - const flattenedProps = getAllProperties( - context, - property.type, - getAllAncestors(property.type) - ).filter( - (p) => p.kind === "property" && !isMetadata(context.program, p.__raw!) - ); - const allPropsReadonly = flattenedProps.every((p) => isReadOnly(p)); - - // For flatten properties in responses: - // - If all properties are readonly, they're flattened at the parent level - // - Otherwise, they're nested under the property name - const flattenPath = allPropsReadonly - ? propertyPath || "item" - : restValue; props.push( - `...${nullOrUndefinedPrefix}${deserializeFunctionName}(${flattenPath})` + `...${nullOrUndefinedPrefix}${deserializeFunctionName}(${restValue})` ); } else { props.push( diff --git a/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md b/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md index 624af97b15..93ae68cee8 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md @@ -52,7 +52,7 @@ export interface Solution { } export function solutionSerializer(item: Solution): any { - return item; + return { properties: undefined, propertiesOptional: undefined }; } /** model interface SolutionProperties */ From 73ec7517f6dd57e92005abd68a00dd248c6c7d65 Mon Sep 17 00:00:00 2001 From: "Jiao Di (MSFT)" Date: Thu, 5 Mar 2026 17:44:48 +0800 Subject: [PATCH 04/10] revert --- .../src/modular/helpers/operationHelpers.ts | 5 ---- .../serialization/buildSerializerFunction.ts | 23 +++---------------- .../flatten-property/src/index.d.ts | 3 --- 3 files changed, 3 insertions(+), 28 deletions(-) diff --git a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts index 4221a54e5f..b8aea5b8d6 100644 --- a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts +++ b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts @@ -1951,11 +1951,6 @@ function getSerializationExpressionForFlatten( !isReadOnly(p) && !isMetadata(context.program, p.__raw!) ); - // If all properties in the flattened model are read-only, omit the field entirely - // so it is not included in the serialized request body. - if (validProps.length === 0) { - return `undefined`; - } const optionalPrefix = property.optional ? `${resolveReference(SerializationHelpers.areAllPropsUndefined)}(${propertyPath}, [${validProps .map((p) => `"${p.name}"`) diff --git a/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts b/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts index 42a98c9095..1f05786480 100644 --- a/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts +++ b/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts @@ -502,26 +502,9 @@ function buildModelTypeSerializer( return ${serializeContent} `); } else { - // For flatten properties, if all properties are readonly, return empty object - // Otherwise, return the item itself - if (options.flatten) { - // Change parameter name to _item to indicate it's intentionally unused - serializerFunction.parameters = [ - { - name: "_item", - type: options.flatten - ? resolveReference(refkey(options.flatten.baseModel)) - : resolveReference(refkey(type)) - } - ]; - output.push(` - return {}; - `); - } else { - output.push(` - return item; - `); - } + output.push(` + return item; + `); } serializerFunction.statements = output; } diff --git a/packages/typespec-ts/test/azureModularIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts b/packages/typespec-ts/test/azureModularIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts index 7af3981259..a6f598fefc 100644 --- a/packages/typespec-ts/test/azureModularIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts +++ b/packages/typespec-ts/test/azureModularIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts @@ -60,9 +60,6 @@ export declare interface Solution { readonly solutionId?: string; readonly title?: string; readonly content?: string; - readonly solutionIdPropertiesOptionalSolutionId?: string; - readonly titlePropertiesOptionalTitle?: string; - readonly contentPropertiesOptionalContent?: string; } export declare interface SolutionProperties { From 1c975da47c4c08bac6dbd338e38b4ecb839dd0b3 Mon Sep 17 00:00:00 2001 From: "Jiao Di (MSFT)" Date: Fri, 6 Mar 2026 10:21:44 +0800 Subject: [PATCH 05/10] fix --- .../modular/serialization/buildSerializerFunction.ts | 10 ++++++++++ .../models/serialization/readonlyFlattenModel.md | 9 ++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts b/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts index 1f05786480..9d25a818d5 100644 --- a/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts +++ b/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts @@ -501,6 +501,16 @@ function buildModelTypeSerializer( output.push(` return ${serializeContent} `); + } else if (options.flatten) { + // Private flatten serializer: all child properties are read-only, so nothing to serialize. + // Rename the parameter to _item to avoid TypeScript unused-variable errors. + const firstParam = serializerFunction.parameters?.[0]; + if (firstParam) { + firstParam.name = "_item"; + } + output.push(` + return {}; + `); } else { output.push(` return item; diff --git a/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md b/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md index 93ae68cee8..6d652acb39 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md @@ -35,6 +35,8 @@ needTCGC: true ## Models ```ts models +import { areAllPropsUndefined } from "../static-helpers/serialization/check-prop-undefined.js"; + /** * 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. @@ -52,7 +54,12 @@ export interface Solution { } export function solutionSerializer(item: Solution): any { - return { properties: undefined, propertiesOptional: undefined }; + return { + properties: _solutionPropertiesSerializer(item), + propertiesOptional: areAllPropsUndefined(item, []) + ? undefined + : _solutionPropertiesOptionalSerializer(item), + }; } /** model interface SolutionProperties */ From bb492b53ad6e6d641a540723eb36aaf7955f9f6d Mon Sep 17 00:00:00 2001 From: "Jiao Di (MSFT)" Date: Fri, 6 Mar 2026 17:03:41 +0800 Subject: [PATCH 06/10] update --- packages/typespec-ts/package.json | 2 +- pnpm-lock.yaml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/typespec-ts/package.json b/packages/typespec-ts/package.json index dc230f4d48..32388d408a 100644 --- a/packages/typespec-ts/package.json +++ b/packages/typespec-ts/package.json @@ -69,7 +69,7 @@ "@typespec/spector": "0.1.0-alpha.24-dev.2", "@typespec/spec-api": "0.1.0-dev.0", "@typespec/tspd": "0.74.0", - "@azure-tools/azure-http-specs": "0.1.0-alpha.38-dev.4", + "@azure-tools/azure-http-specs": "0.1.0-alpha.38-dev.6", "@azure-tools/typespec-autorest": "^0.65.0", "@azure-tools/typespec-azure-core": "^0.65.0", "@azure-tools/typespec-azure-resource-manager": "^0.65.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87c9663e94..0995242462 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -361,8 +361,8 @@ importers: specifier: ^2.3.1 version: 2.5.1 '@azure-tools/azure-http-specs': - specifier: 0.1.0-alpha.38-dev.4 - version: 0.1.0-alpha.38-dev.4(d5a39e74174d9ff5d871e042fc1520d9) + specifier: 0.1.0-alpha.38-dev.6 + version: 0.1.0-alpha.38-dev.6(d5a39e74174d9ff5d871e042fc1520d9) '@azure-tools/typespec-autorest': specifier: ^0.65.0 version: 0.65.0(f818ba7313efc9695bc0d8b1fa87588e) @@ -531,13 +531,13 @@ packages: resolution: {integrity: sha512-X1C7XdyCuo50ch9FzKtTvmK18FgDxxf1Bbt3cSoknQqeDaRegHSSCO+zByq2YA4NvUzKXeZ1engh29IDxZXgpQ==} engines: {node: '>=10.12.0'} - '@azure-tools/azure-http-specs@0.1.0-alpha.38-dev.4': - resolution: {integrity: sha512-RcMWlJoH/ZfFXR4X/nZ/L6siNDtYylYERikcF23JbJ1/SrYsFIS3IpdG4E1YlEMwwfgolL31CDM32+d6g+AViQ==} + '@azure-tools/azure-http-specs@0.1.0-alpha.38-dev.6': + resolution: {integrity: sha512-MXjnMSArEM1+ADCmSrYDsOlTzL9bQWaVMtrTtOK8+2eAL87dZdfxiFuiPr9jWHYC8m8nIwLZ+h4dE7TSQR0KwQ==} engines: {node: '>=20.0.0'} peerDependencies: '@azure-tools/typespec-azure-core': ^0.65.0 || >=0.66.0-dev <0.66.0 '@typespec/compiler': ^1.9.0 - '@typespec/http': ^1.9.0 + '@typespec/http': ^1.9.1 '@typespec/rest': ^0.79.0 || >=0.80.0-dev <0.80.0 '@typespec/versioning': ^0.79.0 || >=0.80.0-dev <0.80.0 '@typespec/xml': ^0.79.0 || >=0.80.0-dev <0.80.0 @@ -5573,7 +5573,7 @@ snapshots: '@azure-tools/tasks': 3.0.255 proper-lockfile: 2.0.1 - '@azure-tools/azure-http-specs@0.1.0-alpha.38-dev.4(d5a39e74174d9ff5d871e042fc1520d9)': + '@azure-tools/azure-http-specs@0.1.0-alpha.38-dev.6(d5a39e74174d9ff5d871e042fc1520d9)': dependencies: '@azure-tools/typespec-azure-core': 0.65.0(@typespec/compiler@1.9.0(@types/node@25.0.8))(@typespec/http@1.9.0(@typespec/compiler@1.9.0(@types/node@25.0.8))(@typespec/streams@0.77.0(@typespec/compiler@1.9.0(@types/node@25.0.8))))(@typespec/rest@0.79.0(@typespec/compiler@1.9.0(@types/node@25.0.8))(@typespec/http@1.9.0(@typespec/compiler@1.9.0(@types/node@25.0.8))(@typespec/streams@0.77.0(@typespec/compiler@1.9.0(@types/node@25.0.8))))) '@typespec/compiler': 1.9.0(@types/node@25.0.8) From 85091425969dbd6fb945faa1a3f057cdf622dce9 Mon Sep 17 00:00:00 2001 From: "Jiao Di (MSFT)" Date: Fri, 6 Mar 2026 17:44:29 +0800 Subject: [PATCH 07/10] update for rlc --- .../client-generator-core/flatten-property/src/index.d.ts | 6 ++---- .../test/azureIntegration/modelFlatten.spec.ts | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/typespec-ts/test/azureIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts b/packages/typespec-ts/test/azureIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts index 17804325f4..2639731cc1 100644 --- a/packages/typespec-ts/test/azureIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts +++ b/packages/typespec-ts/test/azureIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts @@ -133,14 +133,12 @@ export declare interface Routes { export declare interface Solution { name: string; - properties: SolutionProperties; - propertiesOptional?: SolutionProperties; + properties?: SolutionProperties; } export declare interface SolutionOutput { name: string; - properties: SolutionPropertiesOutput; - propertiesOptional?: SolutionPropertiesOutput; + properties?: SolutionPropertiesOutput; } export declare interface SolutionProperties { diff --git a/packages/typespec-ts/test/azureIntegration/modelFlatten.spec.ts b/packages/typespec-ts/test/azureIntegration/modelFlatten.spec.ts index b69aedd8f5..2bd0677430 100644 --- a/packages/typespec-ts/test/azureIntegration/modelFlatten.spec.ts +++ b/packages/typespec-ts/test/azureIntegration/modelFlatten.spec.ts @@ -75,12 +75,12 @@ describe("Flatten Property Rest Client", () => { .put({ body: { name: "foo" - } as any + } }); assert.strictEqual(result.status, "200"); assert.strictEqual(result.body.name, "foo"); - assert.strictEqual((result.body as any).solutionId, "solution1"); - assert.strictEqual((result.body as any).title, "Solution Title"); - assert.strictEqual((result.body as any).content, "Solution Content"); + assert.strictEqual(result.body.properties?.solutionId, "solution1"); + assert.strictEqual(result.body.properties?.title, "Solution Title"); + assert.strictEqual(result.body.properties?.content, "Solution Content"); }); }); From 57192b16d4e00062ea20a025b7bc1806a5691c88 Mon Sep 17 00:00:00 2001 From: "Jiao Di (MSFT)" Date: Mon, 9 Mar 2026 17:04:07 +0800 Subject: [PATCH 08/10] update ut --- .../serialization/readonlyFlattenModel.md | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md b/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md index 6d652acb39..a1bf86bf31 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md @@ -81,3 +81,86 @@ export function _solutionPropertiesOptionalSerializer(_item: Solution): any { return {}; } ``` + +# Should handle flatten model with not all readonly properties correctly + +## TypeSpec + +This is tsp definition. + +```tsp + +model SolutionProperties { + solutionId?: string; + title?: string; + @visibility(Lifecycle.Read) + content?: string; +} +model Solution{ + @Azure.ClientGenerator.Core.Legacy.flattenProperty + properties: SolutionProperties; + @Azure.ClientGenerator.Core.Legacy.flattenProperty + propertiesOptional?: SolutionProperties; +} +op test(@body body:Solution):void; + +``` + +Enable the raw content with TCGC dependency. + +```yaml +needTCGC: true +``` + +## Models + +```ts models +import { areAllPropsUndefined } from "../static-helpers/serialization/check-prop-undefined.js"; + +/** + * 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 Solution */ +export interface Solution { + solutionId?: string; + title?: string; + readonly content?: string; + solutionIdPropertiesOptionalSolutionId?: string; + titlePropertiesOptionalTitle?: string; + readonly contentPropertiesOptionalContent?: string; +} + +export function solutionSerializer(item: Solution): any { + return { + properties: _solutionPropertiesSerializer(item), + propertiesOptional: areAllPropsUndefined(item, ["solutionId", "title"]) + ? undefined + : _solutionPropertiesOptionalSerializer(item), + }; +} + +/** model interface SolutionProperties */ +export interface SolutionProperties { + solutionId?: string; + title?: string; + readonly content?: string; +} + +export function solutionPropertiesSerializer(item: SolutionProperties): any { + return { solutionId: item["solutionId"], title: item["title"] }; +} + +export function _solutionPropertiesSerializer(item: Solution): any { + return { solutionId: item["solutionId"], title: item["title"] }; +} + +export function _solutionPropertiesOptionalSerializer(item: Solution): any { + return { + solutionId: item["solutionIdPropertiesOptionalSolutionId"], + title: item["titlePropertiesOptionalTitle"], + }; +} +``` From 0bbfcd94905047f4fae4148ac63a6049dfa5b5ad Mon Sep 17 00:00:00 2001 From: Mary Gao Date: Wed, 11 Mar 2026 10:13:13 +0800 Subject: [PATCH 09/10] Apply suggestions from code review Co-authored-by: Mary Gao --- .../src/modular/serialization/buildSerializerFunction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts b/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts index 9d25a818d5..3b0fe6bf53 100644 --- a/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts +++ b/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts @@ -501,7 +501,7 @@ function buildModelTypeSerializer( output.push(` return ${serializeContent} `); - } else if (options.flatten) { + } else if (propertiesStr.length && options.flatten) { // Private flatten serializer: all child properties are read-only, so nothing to serialize. // Rename the parameter to _item to avoid TypeScript unused-variable errors. const firstParam = serializerFunction.parameters?.[0]; From d33db3a3796fa10051d035e4145ab2430adeea0d Mon Sep 17 00:00:00 2001 From: "Jiao Di (MSFT)" Date: Wed, 11 Mar 2026 11:26:28 +0800 Subject: [PATCH 10/10] revert --- .../src/modular/serialization/buildSerializerFunction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts b/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts index 3b0fe6bf53..9d25a818d5 100644 --- a/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts +++ b/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts @@ -501,7 +501,7 @@ function buildModelTypeSerializer( output.push(` return ${serializeContent} `); - } else if (propertiesStr.length && options.flatten) { + } else if (options.flatten) { // Private flatten serializer: all child properties are read-only, so nothing to serialize. // Rename the parameter to _item to avoid TypeScript unused-variable errors. const firstParam = serializerFunction.parameters?.[0];