Repository: Azure/typespec-azure
Pull request #2402: [tcgc] ignore visibility when finding response type if it is anonymous model
Author: login: tadelesh
Created: 2025-03-18T06:28:22Z | Merged: 2025-03-18T10:07:29Z

with previous logic, http library will return a created anonymous model without `Create` visibility, and tcgc could not find the original model, but use this anonymous model as the response type (`GetResponse`). see [playground](https://cadlplayground.z22.web.core.windows.net/cadl-azure/?c=QHNlcnZpY2UNCm5hbWVzcGFjZSBUZXN0Q2xpZW50IHsNCiAgbW9kZWzFFsYQICBAdmlzaWJpbGl0eShMaWZlY3ljbGUuQ3JlYXRlKcYjY8UNOiBzdHJpbmc7DQrGF3JlYWTLFSAgfcYab3AgZ2V0KCk6xWg7DQp9DQo%3D&e=%40azure-tools%2Ftypespec-client-generator-core&options=%7B%7D), line 59.
with current logic, tcgc could find the original model and will use that model as the response type (`Test`). see [playground](https://cadlplayground.z22.web.core.windows.net/typespec-azure/prs/2402/?c=QHNlcnZpY2UNCm5hbWVzcGFjZSBUZXN0Q2xpZW50IHsNCiAgbW9kZWzFFsYQICBAdmlzaWJpbGl0eShMaWZlY3ljbGUuQ3JlYXRlKcYjY8UNOiBzdHJpbmc7DQrGF3JlYWTLFSAgfcYab3AgZ2V0KCk6xWg7DQp9DQo%3D&e=%40azure-tools%2Ftypespec-client-generator-core&options=%7B%7D), line59.


Diff:
diff --git a/.chronus/changes/tcgc-fix_visibility-2025-2-18-14-24-28.md b/.chronus/changes/tcgc-fix_visibility-2025-2-18-14-24-28.md
new file mode 100644
index 0000000000..b88255674d
--- /dev/null
+++ b/.chronus/changes/tcgc-fix_visibility-2025-2-18-14-24-28.md
@@ -0,0 +1,7 @@
+---
+changeKind: fix
+packages:
+  - "@azure-tools/typespec-client-generator-core"
+---
+
+Ignore visibility when finding HTTP response type if it is anonymous model.
\ No newline at end of file
diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts
index 8f30690da7..21ca576ca1 100644
--- a/packages/typespec-client-generator-core/src/http.ts
+++ b/packages/typespec-client-generator-core/src/http.ts
@@ -18,6 +18,7 @@ import {
   HttpOperationParameter,
   HttpOperationPathParameter,
   HttpOperationQueryParameter,
+  Visibility,
   getCookieParamOptions,
   getHeaderFieldName,
   getHeaderFieldOptions,
@@ -496,7 +497,7 @@ function getSdkHttpResponseAndExceptions(
         contentTypes = contentTypes.concat(innerResponse.body.contentTypes);
         body =
           innerResponse.body.type.kind === "Model"
-            ? getEffectivePayloadType(context, innerResponse.body.type)
+            ? getEffectivePayloadType(context, innerResponse.body.type, Visibility.Read)
             : innerResponse.body.type;
         if (getStreamMetadata(context.program, innerResponse)) {
           // map stream response body type to bytes
diff --git a/packages/typespec-client-generator-core/src/public-utils.ts b/packages/typespec-client-generator-core/src/public-utils.ts
index 3ec683401b..deb7d1bc98 100644
--- a/packages/typespec-client-generator-core/src/public-utils.ts
+++ b/packages/typespec-client-generator-core/src/public-utils.ts
@@ -17,7 +17,14 @@ import {
   isService,
   resolveEncodedName,
 } from "@typespec/compiler";
-import { HttpOperation, getHttpOperation, getHttpPart, isMetadata } from "@typespec/http";
+import {
+  HttpOperation,
+  Visibility,
+  getHttpOperation,
+  getHttpPart,
+  isMetadata,
+  isVisible,
+} from "@typespec/http";
 import { Version, getVersions } from "@typespec/versioning";
 import { pascalCase } from "change-case";
 import pluralize from "pluralize";
@@ -112,15 +119,20 @@ export function getClientNamespaceString(context: TCGCContext): string | undefin
 }
 
 /**
- * If the given type is an anonymous model and all of its properties excluding
- * header/query/path/status-code are sourced from a named model, returns that original named model.
- * Otherwise the given type is returned unchanged.
+ * If the given type is an anonymous model, returns a named model with same shape.
+ * The finding logic will ignore all the properties of header/query/path/status-code metadata,
+ * as well as the properties that are not visible in the given visibility if provided.
+ * If the model found is also anonymous, the input type is returned unchanged.
  *
  * @param context
  * @param type
  * @returns
  */
-export function getEffectivePayloadType(context: TCGCContext, type: Model): Model {
+export function getEffectivePayloadType(
+  context: TCGCContext,
+  type: Model,
+  visibility?: Visibility,
+): Model {
   const program = context.program;
 
   // if a type has name, we should resolve the name
@@ -135,7 +147,10 @@ export function getEffectivePayloadType(context: TCGCContext, type: Model): Mode
   const effective = getEffectiveModelType(
     program,
     type,
-    (t) => !isMetadata(context.program, t) && !hasNoneVisibility(context, t),
+    (t) =>
+      !isMetadata(context.program, t) &&
+      !hasNoneVisibility(context, t) &&
+      (visibility === undefined || isVisible(program, t, visibility)),
   );
   if (effective.name) {
     return effective;
diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts
index a5cf3723e6..d3cf7552c4 100644
--- a/packages/typespec-client-generator-core/src/types.ts
+++ b/packages/typespec-client-generator-core/src/types.ts
@@ -1752,7 +1752,7 @@ function updateTypesFromOperation(
       if (innerResponse.body?.type && !isNeverOrVoidType(innerResponse.body.type)) {
         const body =
           innerResponse.body.type.kind === "Model"
-            ? getEffectivePayloadType(context, innerResponse.body.type)
+            ? getEffectivePayloadType(context, innerResponse.body.type, Visibility.Read)
             : innerResponse.body.type;
         const sdkType = diagnostics.pipe(getClientTypeWithDiagnostics(context, body, operation));
         if (generateConvenient) {
diff --git a/packages/typespec-client-generator-core/test/methods/responses.test.ts b/packages/typespec-client-generator-core/test/methods/responses.test.ts
index 7809747f8f..7263c65941 100644
--- a/packages/typespec-client-generator-core/test/methods/responses.test.ts
+++ b/packages/typespec-client-generator-core/test/methods/responses.test.ts
@@ -1,6 +1,6 @@
 import { deepStrictEqual, ok, strictEqual } from "assert";
 import { beforeEach, it } from "vitest";
-import { SdkHttpOperation, SdkServiceMethod } from "../../src/interfaces.js";
+import { SdkHttpOperation, SdkMethodResponse, SdkServiceMethod } from "../../src/interfaces.js";
 import { SdkTestRunner, createSdkTestRunner } from "../test-host.js";
 import { getServiceMethodOfClient } from "../utils.js";
 
@@ -371,3 +371,28 @@ it("description shall be included in response", async () => {
   strictEqual(responses.length, 1);
   strictEqual(responses[0].description, "description on response");
 });
+
+it("response body with non-read visibility", async () => {
+  await runner.compile(`
+    @service
+    namespace TestClient {
+      model Test {
+        @visibility(Lifecycle.Create)
+        create: string;
+
+        read: string;
+      }
+
+      op get(): Test;
+    }
+  `);
+  const models = runner.context.sdkPackage.models;
+  strictEqual(models.length, 1);
+  const model = models[0];
+  strictEqual(model.name, "Test");
+  const client = runner.context.sdkPackage.clients[0];
+  ok(client);
+  const method = client.methods[0];
+  ok(method);
+  strictEqual((method.response as SdkMethodResponse).type, model);
+});


Reviews:
user: login: weidongxu-microsoft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T07:03:43Z
user: login: tadelesh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T07:22:00Z
user: login: weidongxu-microsoft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T07:24:13Z
user: login: weidongxu-microsoft
body: LGTM

Not sure how much its impact on test. Hope real SDK won't be affected (I think it unlikely).
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-18T07:31:18Z
user: login: msyyc
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-18T07:35:28Z
user: login: tadelesh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T07:43:29Z

Review comments:
diff_hunk: @@ -120,7 +127,11 @@ export function getClientNamespaceString(context: TCGCContext): string | undefin
  * @param type
  * @returns
  */
-export function getEffectivePayloadType(context: TCGCContext, type: Model): Model {
+export function getEffectivePayloadType(
+  context: TCGCContext,
+  type: Model,
+  visibility?: Visibility,
+): Model {
path: packages/typespec-client-generator-core/src/public-utils.ts
user: login: weidongxu-microsoft
body: The docstring above need to be updated.

I am not sure what does `Visibility.Read` mean in caller.
In the test, `Visibility.Create` would be not affect the effective type. Then what kind of property would affect? `@visibility(Visibility.Read) prop` -- but read-only property seems should not affect as well?
created_at: 2025-03-18T07:03:43Z
author_association: MEMBER
diff_hunk: @@ -120,7 +127,11 @@ export function getClientNamespaceString(context: TCGCContext): string | undefin
  * @param type
  * @returns
  */
-export function getEffectivePayloadType(context: TCGCContext, type: Model): Model {
+export function getEffectivePayloadType(
+  context: TCGCContext,
+  type: Model,
+  visibility?: Visibility,
+): Model {
path: packages/typespec-client-generator-core/src/public-utils.ts
user: login: tadelesh
body: doc updated. the visibility here means only take the properties with the visibility provided into account when finding the potential named model.
created_at: 2025-03-18T07:22:00Z
author_association: MEMBER
diff_hunk: @@ -120,7 +127,11 @@ export function getClientNamespaceString(context: TCGCContext): string | undefin
  * @param type
  * @returns
  */
-export function getEffectivePayloadType(context: TCGCContext, type: Model): Model {
+export function getEffectivePayloadType(
+  context: TCGCContext,
+  type: Model,
+  visibility?: Visibility,
+): Model {
path: packages/typespec-client-generator-core/src/public-utils.ts
user: login: weidongxu-microsoft
body: So, the test case works, because the response from http lib is
```
{
  read: string
}
```
and via this `getEffectivePayloadType(..., Visibility.READ)`, it would map to `Test` model?

If it already
```
model Test {
    @visibility(Lifecycle.Read)
    readOnly: string;

    read: string;
}
```
this would directly be `Test` model in response of http lib?
created_at: 2025-03-18T07:24:13Z
author_association: MEMBER
diff_hunk: @@ -120,7 +127,11 @@ export function getClientNamespaceString(context: TCGCContext): string | undefin
  * @param type
  * @returns
  */
-export function getEffectivePayloadType(context: TCGCContext, type: Model): Model {
+export function getEffectivePayloadType(
+  context: TCGCContext,
+  type: Model,
+  visibility?: Visibility,
+): Model {
path: packages/typespec-client-generator-core/src/public-utils.ts
user: login: tadelesh
body: correct.
created_at: 2025-03-18T07:43:29Z
author_association: MEMBER

PR comments:
user: login: azure-sdk
created_at: 2025-03-18T06:30:32Z
body: <!--chronus-github-change-commenter-->
All changed packages have been documented.
 - :white_check_mark: `@azure-tools/typespec-client-generator-core`

<details>
<summary>Show changes</summary>

### `@azure-tools/typespec-client-generator-core` - _fix_ [✏️](https://github.com/Azure/typespec-azure/edit/tcgc/fix_visibility/.chronus/changes/tcgc-fix_visibility-2025-2-18-14-24-28.md?pr=/Azure/typespec-azure/pull/2402)
> Ignore visibility when finding HTTP response type if it is anonymous model.
</details>

author_association: COLLABORATOR
user: login: azure-sdk
created_at: 2025-03-18T06:42:16Z
body: <!-- _TSP_TRYIT_COMMENT_ -->
You can try these changes here

| [🛝 Playground]( https://cadlplayground.z22.web.core.windows.net/typespec-azure/prs/2402/)|[🌐 Website](https://tspwebsitepr.z22.web.core.windows.net/typespec-azure/prs/2402/) |
| ---|--- |
author_association: COLLABORATOR
user: login: tadelesh
created_at: 2025-03-18T07:44:13Z
body: > LGTM
> 
> Not sure how much its impact on test. Hope real SDK won't be affected (I think it unlikely).

i doubt at least batch will be impacted?
author_association: MEMBER
user: login: weidongxu-microsoft
created_at: 2025-03-18T07:53:45Z
body: > i doubt at least batch will be impacted?

`BatchCertificate`? Service dev seems not expose this class and the API in Java SDK (unsure why -- they've already made the API `internal`).
author_association: MEMBER