|
| 1 | +# TCGC Monthly Newsletter — `@azure-tools/typespec-client-generator-core` 0.68 |
| 2 | + |
| 3 | +Hey everyone! This newsletter covers the latest TCGC releases (0.67.2 through 0.68.0) with the new `exact()` function for precise client naming, `serializationOptions` for body/response types, enhanced hierarchy building, and important bug fixes. As always, I welcome any and all feedback! |
| 4 | + |
| 5 | +## ⭐ What's New in This Release |
| 6 | + |
| 7 | +- `exact()` Function for `@clientName` ⭐ (Major Feature) — [docs](https://azure.github.io/typespec-azure/docs/howtos/generate-client-libraries/09renaming/#preserving-exact-casing) |
| 8 | +- `serializationOptions` on Body Parameters and Responses ⭐ (Major Feature) — [docs](https://azure.github.io/typespec-azure/docs/howtos/generate-client-libraries/08types/) |
| 9 | +- Enhanced `@Legacy.hierarchyBuilding` — Arbitrary Inheritance Replacement — [docs](https://azure.github.io/typespec-azure/docs/howtos/generate-client-libraries/11hierarchybuilding/) |
| 10 | +- `inconsistent-multiple-service-dependency` Warning — [docs](https://azure.github.io/typespec-azure/docs/howtos/generate-client-libraries/03client/#one-client-from-multiple-services) |
| 11 | +- Bug Fixes & Improvements |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## ⚠️ Emitter Behavior Changes |
| 16 | + |
| 17 | +Emitter authors, heads up! This release includes a **breaking change** that requires attention. See the detailed section below for migration guidance. |
| 18 | + |
| 19 | +- **Multi-content-type `accept` parameter is now a string constant** — When an operation's response declares multiple content types (e.g., `Http.File<"image/png" | "image/jpeg">`), the synthetic `accept` parameter is now a single string constant (comma-joined, structured types listed first) instead of an enum. This avoids incorrectly modeling such operations as content negotiation. Use `@sharedRoute` to split an operation if real content negotiation is required. |
| 20 | +- **External alternate types no longer leak into `sdkPackage`** — Types that are only used within external alternate types are no longer included in `sdkPackage.models`, `sdkPackage.enums`, or `sdkPackage.unions`. If your emitter previously relied on these types being present in the package-level collections, they will now be absent. |
| 21 | + |
| 22 | +--- |
| 23 | + |
| 24 | +## ⭐ `exact()` Function for `@clientName` (0.68.0) |
| 25 | + |
| 26 | +📖 **Docs**: [Preserving Exact Casing](https://azure.github.io/typespec-azure/docs/howtos/generate-client-libraries/09renaming/#preserving-exact-casing) · [`@clientName` reference](https://azure.github.io/typespec-azure/docs/libraries/typespec-client-generator-core/reference/decorators/#@Azure.ClientGenerator.Core.clientName) |
| 27 | + |
| 28 | +A new `exact()` function that preserves client names **without language-specific casing transformations**. Previously, names provided via `@clientName` would still be subject to emitter-specific casing rules (e.g., `PascalCase` for C#, `snake_case` for Python). Now you can opt out of those transformations entirely. |
| 29 | + |
| 30 | +### How It Works |
| 31 | + |
| 32 | +Wrap the name string in `exact()` when calling `@@clientName`: |
| 33 | + |
| 34 | +```typespec |
| 35 | +// Without exact — emitter may transform casing |
| 36 | +@@clientName(MyService.TestModel, "RenamedModel"); |
| 37 | +
|
| 38 | +// With exact — name is preserved as-is, no casing transformation |
| 39 | +#suppress "experimental-feature" "using exact" |
| 40 | +@@clientName(MyService.TestModel, exact("hello_world")); |
| 41 | +``` |
| 42 | + |
| 43 | +Works with language scoping too: |
| 44 | + |
| 45 | +```typespec |
| 46 | +// Only Python uses the exact name; other languages use the original |
| 47 | +#suppress "experimental-feature" "using exact" |
| 48 | +@@clientName(MyService.TestModel, exact("hello_world"), "python"); |
| 49 | +``` |
| 50 | + |
| 51 | +Applies to models, properties, enums, and operations: |
| 52 | + |
| 53 | +```typespec |
| 54 | +#suppress "experimental-feature" "using exact" |
| 55 | +@@clientName(MyService.TestModel.myProp, exact("my_exact_prop")); |
| 56 | +
|
| 57 | +#suppress "experimental-feature" "using exact" |
| 58 | +@@clientName(MyService.Status, exact("my_status_enum")); |
| 59 | +
|
| 60 | +#suppress "experimental-feature" "using exact" |
| 61 | +@@clientName(MyService.testOp, exact("my_exact_op")); |
| 62 | +``` |
| 63 | + |
| 64 | +What Emitter Authors Gain: A new `isExactName` boolean field on `SdkModelType`, `SdkEnumType`, `SdkUnionType`, and `SdkModelPropertyTypeBase`. When `isExactName` is `true`, emitters should skip their language-specific casing transformations and use the name verbatim. |
| 65 | + |
| 66 | +--- |
| 67 | + |
| 68 | +## ⭐ `serializationOptions` on Body Parameters and Responses (0.68.0) |
| 69 | + |
| 70 | +📖 **Docs**: [Generated Types](https://azure.github.io/typespec-azure/docs/howtos/generate-client-libraries/08types/) |
| 71 | + |
| 72 | +A new `serializationOptions` property on `SdkBodyParameter` and `SdkHttpResponse`/`SdkHttpErrorResponse` that tells emitters **how to serialize/deserialize the request or response body**. This is especially important for **basic (non-model) body types** — scalars like `string`, `bytes`, or `int32` — where previously emitters had no way to know the serialization format from the type alone. |
| 73 | + |
| 74 | +### The Problem |
| 75 | + |
| 76 | +When the body type is a model, emitters could already inspect the model's own `serializationOptions` to determine JSON/XML wire names. But when the body is a basic type (e.g., `@body body: string` or `@body body: bytes`), there's no model to inspect — emitters had to parse content-type strings themselves to figure out how to serialize. |
| 77 | + |
| 78 | +### The Solution |
| 79 | + |
| 80 | +`serializationOptions` is now available directly on `SdkBodyParameter` and `SdkHttpResponseBase`, providing format info regardless of body type: |
| 81 | + |
| 82 | +```typescript |
| 83 | +// Emitter code — determine how to serialize a basic-type body |
| 84 | +const bodyParam = httpOperation.bodyParam; |
| 85 | + |
| 86 | +if (bodyParam.serializationOptions.json) { |
| 87 | + // Body should be serialized as JSON (e.g., a raw string sent as JSON) |
| 88 | +} else if (bodyParam.serializationOptions.xml) { |
| 89 | + // Body should be serialized as XML |
| 90 | +} else if (bodyParam.serializationOptions.binary) { |
| 91 | + // Body is binary (file upload, octet-stream, etc.) |
| 92 | + const { isFile, isText, contentTypes } = bodyParam.serializationOptions.binary; |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +```typespec |
| 97 | +// Basic type body — emitters previously couldn't determine format |
| 98 | +op uploadRawData(@header contentType: "application/octet-stream", @body data: bytes): void; |
| 99 | +// → bodyParam.serializationOptions.binary is populated |
| 100 | +
|
| 101 | +op sendMessage(@header contentType: "application/json", @body message: string): void; |
| 102 | +// → bodyParam.serializationOptions.json is populated |
| 103 | +``` |
| 104 | + |
| 105 | +For model bodies, `serializationOptions` remains available on the model type itself and its properties (providing wire names per format), and is now **also** mirrored on the body parameter for consistency: |
| 106 | + |
| 107 | +```typespec |
| 108 | +model Blob { |
| 109 | + @encodedName("application/json", "newId") |
| 110 | + id: string; |
| 111 | +} |
| 112 | +
|
| 113 | +op test(@body body: Blob): void; |
| 114 | +// → bodyParam.serializationOptions.json is populated |
| 115 | +// → model.properties[0].serializationOptions.json.name === "newId" |
| 116 | +``` |
| 117 | + |
| 118 | +What Emitter Authors Gain: A single, consistent way to determine serialization format for **all** body types — models, scalars, and files alike. No more parsing content-type strings manually for basic-type bodies. |
| 119 | + |
| 120 | +--- |
| 121 | + |
| 122 | +## 🏗️ Enhanced `@Legacy.hierarchyBuilding` — Arbitrary Inheritance Replacement (0.68.0) |
| 123 | + |
| 124 | +📖 **Docs**: [Legacy Hierarchy Building](https://azure.github.io/typespec-azure/docs/howtos/generate-client-libraries/11hierarchybuilding/) |
| 125 | + |
| 126 | +The `@Legacy.hierarchyBuilding` decorator no longer requires the target to be a property-superset of the new base model. Properties contributed by removed intermediate parents are now **automatically lifted** onto the target, preserving the observable property set. |
| 127 | + |
| 128 | +### Before (0.67.x) |
| 129 | + |
| 130 | +`@Legacy.hierarchyBuilding` could only rebase to a sibling that already had all the parent's properties. Attempting to rebase to an unrelated model would fail. |
| 131 | + |
| 132 | +### After (0.68.0) |
| 133 | + |
| 134 | +Rebase works to arbitrary ancestor models. Missing properties from removed intermediates are lifted: |
| 135 | + |
| 136 | +```typespec |
| 137 | +@discriminator("kind") |
| 138 | +model A { |
| 139 | + kind: string; |
| 140 | +} |
| 141 | +
|
| 142 | +alias BContent = { |
| 143 | + foo: string; |
| 144 | +}; |
| 145 | +
|
| 146 | +model B extends A { |
| 147 | + kind: "B"; |
| 148 | + ...BContent; |
| 149 | +} |
| 150 | +
|
| 151 | +// C now inherits from B instead of A — foo is NOT required on C |
| 152 | +// because BContent is already on B |
| 153 | +@Legacy.hierarchyBuilding(B) |
| 154 | +model C extends A { |
| 155 | + kind: "C"; |
| 156 | + ...BContent; |
| 157 | + bar: string; |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +Result: `C.baseModel` becomes `B`, and `C.properties` contains only `kind` and `bar` (since `foo` is inherited from `B`). |
| 162 | + |
| 163 | +What Emitter Authors Gain: More flexibility for ARM Compute-style migrations where models need to be rebased to different points in the hierarchy without redeclaring all intermediate properties. |
| 164 | + |
| 165 | +--- |
| 166 | + |
| 167 | +## 🔔 `inconsistent-multiple-service-dependency` Warning (0.68.0) |
| 168 | + |
| 169 | +📖 **Docs**: [Multi-Service Clients](https://azure.github.io/typespec-azure/docs/howtos/generate-client-libraries/03client/#one-client-from-multiple-services) |
| 170 | + |
| 171 | +A new warning diagnostic that fires when services merged into a single client (via `autoMergeService`) declare **diverging `@useDependency` versions** for the same shared library (e.g., ARM common-types v5 vs. v6). |
| 172 | + |
| 173 | +What Spec Authors Should Do: Align shared dependency versions across all services merged into one client to avoid generating duplicated or diverged models in the SDK. |
| 174 | + |
| 175 | +--- |
| 176 | + |
| 177 | +## 🐛 Bug Fixes & Improvements |
| 178 | + |
| 179 | +Related docs: [`@clientLocation`](https://azure.github.io/typespec-azure/docs/howtos/generate-client-libraries/04method/#using-clientlocation-to-control-parameter-placement) · [`@apiVersion`](https://azure.github.io/typespec-azure/docs/libraries/typespec-client-generator-core/reference/decorators/#@Azure.ClientGenerator.Core.apiVersion) |
| 180 | + |
| 181 | +**`@clientLocation` with `@override` — Wrong `methodParameterSegments` (0.67.3 + 0.68.0)** |
| 182 | + |
| 183 | +- Issue: Operations using both `@clientLocation` and `@override` produced incorrect `methodParameterSegments`. |
| 184 | +- Fix: [#4302](https://github.com/Azure/typespec-azure/pull/4302), [#4344](https://github.com/Azure/typespec-azure/pull/4344) |
| 185 | + |
| 186 | +**`@apiVersion(false)` Ignored by `isOnClient()` (0.67.2)** |
| 187 | + |
| 188 | +- Issue: `@apiVersion(false)` was ignored when other operations had already elevated an api-version parameter to client level. |
| 189 | +- Fix: [#4234](https://github.com/Azure/typespec-azure/pull/4234) |
| 190 | + |
| 191 | +**Duplicate Client Entries in Multi-Service (0.67.2)** |
| 192 | + |
| 193 | +- Issue: Calling `createSdkContext` multiple times or merging sub clients with the same name in multi-service cases produced duplicate client entries. |
| 194 | +- Fix: [#4253](https://github.com/Azure/typespec-azure/pull/4253) |
| 195 | + |
| 196 | +**Readonly Property Usage Propagation (0.67.2)** |
| 197 | + |
| 198 | +- Issue: Readonly properties didn't properly strip the `Input` flag from combined usage values, and `ignoreSubTypeStack` was unbalanced. |
| 199 | +- Fix: [#4235](https://github.com/Azure/typespec-azure/pull/4235) |
| 200 | + |
| 201 | +**Error Response in Intersection Types (0.67.1)** |
| 202 | + |
| 203 | +- Issue: Error responses in intersection types (e.g., `ArmAcceptedResponse & ErrorResponse`) weren't classified as exceptions, causing false `unexpected-pageable-operation-return-type` diagnostics. |
| 204 | +- Fix: [#4215](https://github.com/Azure/typespec-azure/pull/4215) |
| 205 | + |
| 206 | +**`bytes` Encoding in `HttpPart` for Multipart (0.67.4)** |
| 207 | + |
| 208 | +- Issue: `bytes` in `HttpPart` for `multipart/form-data` was incorrectly encoded as `base64` instead of `bytes`. |
| 209 | +- Fix: [#4345](https://github.com/Azure/typespec-azure/pull/4345) |
| 210 | + |
| 211 | +**Wrong API Version Param Judgement (0.68.0)** |
| 212 | + |
| 213 | +- Issue: A body model property named `apiVersion`/`api-version` was incorrectly flagged as `isApiVersionParam` with a service-derived `clientDefaultValue`. Only operation parameters should be considered API version parameters. |
| 214 | +- Fix: [#4341](https://github.com/Azure/typespec-azure/pull/4341), [#4386](https://github.com/Azure/typespec-azure/pull/4386) |
| 215 | + |
| 216 | +**`@responseAsBool` on HEAD Operations (0.68.0)** |
| 217 | + |
| 218 | +- Issue: `@responseAsBool` incorrectly set `bodyType` on HEAD operation HTTP responses. |
| 219 | +- Fix: [#4343](https://github.com/Azure/typespec-azure/pull/4343) |
| 220 | + |
| 221 | +**External Alternate Types Leaking into Package (0.68.0)** |
| 222 | + |
| 223 | +- Issue: Types only used within external alternate types were incorrectly included in `sdkPackage` models, enums, or unions. |
| 224 | +- Fix: [#4236](https://github.com/Azure/typespec-azure/pull/4236) |
| 225 | + |
| 226 | +--- |
| 227 | + |
| 228 | +## 🎉 The Bottom Line |
| 229 | + |
| 230 | +The headline feature is **`exact()`** — a simple but powerful addition that lets spec authors preserve client names exactly as written, bypassing emitter casing transformations. This is especially useful for names with deliberate casing (acronyms, legacy names, cross-language consistency). |
| 231 | + |
| 232 | +**`serializationOptions`** on body parameters and responses gives emitters a clear, format-aware contract for serialization — no more inferring JSON vs. XML from content-type strings. |
| 233 | + |
| 234 | +The **breaking change** to multi-content-type `accept` parameters (string constant instead of enum) properly distinguishes "accept all these types" from true content negotiation. If you have emitter logic that pattern-matches on enum-typed accept parameters, update it to handle string constants. |
| 235 | + |
| 236 | +Special thanks to all emitter authors for your continued collaboration and feedback! 🙌 |
| 237 | + |
| 238 | +Thanks, @Chenjie and @Isabella |
0 commit comments