From 0523d1167eb0ca4836db500333f9f28b8b4743ae Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 3 Mar 2026 10:58:41 -0500 Subject: [PATCH 1/4] Replace warning to use implicitOptionality in `@patch` with deprecation for old behavior --- .../specs/type/model/visibility/main.tsp | 1 + packages/http/README.md | 7 +++---- packages/http/generated-defs/TypeSpec.Http.ts | 7 ++----- packages/http/lib/decorators.tsp | 8 +++----- packages/http/src/decorators.ts | 10 +++++++++- packages/http/src/lib.ts | 4 ++-- packages/http/src/parameters.ts | 20 ------------------- packages/http/test/http-decorators.test.ts | 20 +++++++++++++++++++ packages/http/test/merge-patch.test.ts | 1 - packages/openapi3/test/metadata.test.ts | 11 +++++++++- packages/rest/lib/resource.tsp | 5 +++++ .../libraries/http/reference/decorators.md | 7 +++---- 12 files changed, 58 insertions(+), 43 deletions(-) diff --git a/packages/http-specs/specs/type/model/visibility/main.tsp b/packages/http-specs/specs/type/model/visibility/main.tsp index 80be39ab019..bb4c63c652d 100644 --- a/packages/http-specs/specs/type/model/visibility/main.tsp +++ b/packages/http-specs/specs/type/model/visibility/main.tsp @@ -98,6 +98,7 @@ op putModel(@body input: VisibilityModel): void; } ``` """) +#suppress "@typespec/http/deprecated-implicit-optionality" "for legacy test" @patch(#{ implicitOptionality: true }) op patchModel(@body input: VisibilityModel): void; diff --git a/packages/http/README.md b/packages/http/README.md index b2dd9e05494..3600a1dfb10 100644 --- a/packages/http/README.md +++ b/packages/http/README.md @@ -344,11 +344,10 @@ Specify the HTTP verb for the target operation to be `PATCH`. @patch op update(pet: Pet): void; ``` +###### Using MergePatch template for proper merge-patch semantics + ```typespec -// Disable implicit optionality, making the body of the PATCH operation use the -// optionality as defined in the `Pet` model. -@patch(#{ implicitOptionality: false }) -op update(pet: Pet): void; +@patch op update(@body pet: MergePatchUpdate): void; ``` #### `@path` diff --git a/packages/http/generated-defs/TypeSpec.Http.ts b/packages/http/generated-defs/TypeSpec.Http.ts index 025e8a09e56..c430839aa82 100644 --- a/packages/http/generated-defs/TypeSpec.Http.ts +++ b/packages/http/generated-defs/TypeSpec.Http.ts @@ -244,12 +244,9 @@ export type PostDecorator = ( * ```typespec * @patch op update(pet: Pet): void; * ``` - * @example + * @example Using MergePatch template for proper merge-patch semantics * ```typespec - * // Disable implicit optionality, making the body of the PATCH operation use the - * // optionality as defined in the `Pet` model. - * @patch(#{ implicitOptionality: false }) - * op update(pet: Pet): void; + * @patch op update(...MergePatchUpdate): void; * ``` */ export type PatchDecorator = ( diff --git a/packages/http/lib/decorators.tsp b/packages/http/lib/decorators.tsp index 444f66d6219..f871816e753 100644 --- a/packages/http/lib/decorators.tsp +++ b/packages/http/lib/decorators.tsp @@ -266,6 +266,7 @@ model PatchOptions { /** * If set to `false`, disables the implicit transform that makes the body of a * PATCH operation deeply optional. + * @deprecated Use MergePatch templates instead. */ implicitOptionality?: boolean; } @@ -281,12 +282,9 @@ model PatchOptions { * @patch op update(pet: Pet): void; * ``` * - * @example + * @example Using MergePatch template for proper merge-patch semantics * ```typespec - * // Disable implicit optionality, making the body of the PATCH operation use the - * // optionality as defined in the `Pet` model. - * @patch(#{ implicitOptionality: false }) - * op update(pet: Pet): void; + * @patch op update(@body pet: MergePatchUpdate): void; * ``` */ extern dec patch(target: Operation, options?: valueof PatchOptions); diff --git a/packages/http/src/decorators.ts b/packages/http/src/decorators.ts index cb5457b1e93..aad49bd3302 100644 --- a/packages/http/src/decorators.ts +++ b/packages/http/src/decorators.ts @@ -416,7 +416,15 @@ export const $patch: PatchDecorator = ( ) => { _patch(context, entity); - if (options) setPatchOptions(context.program, entity, options); + if (options) { + if (options.implicitOptionality === true) { + reportDiagnostic(context.program, { + code: "deprecated-implicit-optionality", + target: entity, + }); + } + setPatchOptions(context.program, entity, options); + } }; /** diff --git a/packages/http/src/lib.ts b/packages/http/src/lib.ts index 7cb09b523bc..76d0daea283 100644 --- a/packages/http/src/lib.ts +++ b/packages/http/src/lib.ts @@ -195,10 +195,10 @@ export const $lib = createTypeSpecLibrary({ default: paramMessage`The 'contents' property of the file model must be a scalar type that extends 'string' or 'bytes'. Found '${"type"}'.`, }, }, - "patch-implicit-optional": { + "deprecated-implicit-optionality": { severity: "warning", messages: { - default: `Patch operation stopped applying an implicit optional transform to the body in 1.0.0. Use @patch(#{implicitOptionality: true}) to restore the old behavior.`, + default: `The implicitOptionality option is deprecated. Use MergePatch templates to define the body of a PATCH operation instead.`, }, }, "merge-patch-contains-null": { diff --git a/packages/http/src/parameters.ts b/packages/http/src/parameters.ts index 9a60336a5c6..9813f53aad0 100644 --- a/packages/http/src/parameters.ts +++ b/packages/http/src/parameters.ts @@ -7,12 +7,9 @@ import { } from "@typespec/compiler"; import { getOperationVerb, - getPatchOptions, getPathOptions, getQueryOptions, } from "./decorators.js"; -import { isMergePatchBody } from "./experimental/merge-patch/internal.js"; -import { createDiagnostic } from "./lib.js"; import { resolveRequestVisibility } from "./metadata.js"; import { HttpPayloadDisposition, resolveHttpPayload } from "./payload.js"; import { @@ -123,23 +120,6 @@ function getOperationParametersForVerb( }, }), ); - const implicitOptionality = getPatchOptions(program, operation)?.implicitOptionality; - // TODO: remove in 6month after 1.0.0. (November 2025) - if ( - verb === "patch" && - resolvedBody && - implicitOptionality === undefined && - !isMergePatchBody(program, resolvedBody?.type) && - !resolvedBody.contentTypes.includes("application/merge-patch+json") // Above statement doesn't detect Spread merge patch - ) { - diagnostics.add( - createDiagnostic({ - code: "patch-implicit-optional", - target: operation, - }), - ); - } - for (const item of metadata) { switch (item.kind) { case "contentType": diff --git a/packages/http/test/http-decorators.test.ts b/packages/http/test/http-decorators.test.ts index 1e88a9f7c63..004564e77d8 100644 --- a/packages/http/test/http-decorators.test.ts +++ b/packages/http/test/http-decorators.test.ts @@ -50,6 +50,26 @@ describe("http: decorators", () => { message: `Argument of type '"/test"' is not assignable to parameter of type 'valueof TypeSpec.Http.PatchOptions'`, }); }); + + it(`@patch emits deprecation warning when implicitOptionality: true`, async () => { + const diagnostics = await Tester.diagnose(` + @patch(#{ implicitOptionality: true }) op test(): string; + `); + + expectDiagnostics(diagnostics, { + code: "@typespec/http/deprecated-implicit-optionality", + message: + "The implicitOptionality option is deprecated. Use MergePatch templates to define the body of a PATCH operation instead.", + }); + }); + + it(`@patch does not emit deprecation warning when implicitOptionality: false`, async () => { + const diagnostics = await Tester.diagnose(` + @patch(#{ implicitOptionality: false }) op test(): string; + `); + + expectDiagnosticEmpty(diagnostics); + }); }); describe("@header", () => { diff --git a/packages/http/test/merge-patch.test.ts b/packages/http/test/merge-patch.test.ts index 948ca8729c0..1cbac9f7d55 100644 --- a/packages/http/test/merge-patch.test.ts +++ b/packages/http/test/merge-patch.test.ts @@ -225,7 +225,6 @@ describe("http operation support", () => { name: string; description?: string; } - #suppress "@typespec/http/patch-implicit-optional" "For test only ignore correct merge patch" @patch op update(@header("Content-type") contentType: "application/json", ...MergePatchUpdate): void;`); expectDiagnostics(diag, { code: "@typespec/http/merge-patch-content-type", diff --git a/packages/openapi3/test/metadata.test.ts b/packages/openapi3/test/metadata.test.ts index 727bd7c79f2..7f5e60a2cd4 100644 --- a/packages/openapi3/test/metadata.test.ts +++ b/packages/openapi3/test/metadata.test.ts @@ -99,6 +99,7 @@ worksFor(supportedVersions, ({ openApiFor }) => { @visibility(Lifecycle.Read, Lifecycle.Update, Lifecycle.Create) ruc?: string; } @parameterVisibility(Lifecycle.Create, Lifecycle.Update) + #suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior" @route("/") @patch(#{implicitOptionality: true}) op createOrUpdate(...M): M; `); @@ -176,7 +177,8 @@ worksFor(supportedVersions, ({ openApiFor }) => { person: Person; relationship: string; } - @route("/") @patch(#{implicitOptionality: true}) op update(...Person): Person; + @route("/") #suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior" + @patch(#{implicitOptionality: true}) op update(...Person): Person; `); const response = res.paths["/"].patch.responses["200"].content["application/json"].schema; @@ -217,6 +219,7 @@ worksFor(supportedVersions, ({ openApiFor }) => { weight: float64; } @post op create(...Widget): void; + #suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior" @patch(#{implicitOptionality: true}) op update(...Widget): void; `); @@ -358,6 +361,7 @@ worksFor(supportedVersions, ({ openApiFor }) => { @get get(...M): M; @post create(...M): M; @put createOrUpdate(...M): M; + #suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior" @patch(#{implicitOptionality: true}) update(...M): M; @delete delete(...M): void; } @@ -367,6 +371,7 @@ worksFor(supportedVersions, ({ openApiFor }) => { @get get(...D): D; @post create(...D): D; @put createOrUpdate(...D): D; + #suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior" @patch(#{implicitOptionality: true}) update(...D): D; @delete delete(...D): void; } @@ -376,6 +381,7 @@ worksFor(supportedVersions, ({ openApiFor }) => { @get op get(id: string): R; @post op create(...R): R; @put op createOrUpdate(...R): R; + #suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior" @patch(#{implicitOptionality: true}) op update(...R): R; @delete op delete(...D): void; } @@ -384,6 +390,7 @@ worksFor(supportedVersions, ({ openApiFor }) => { @get op get(id: string): U; @post op create(...U): U; @put op createOrUpdate(...U): U; + #suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior" @patch(#{implicitOptionality: true}) op update(...U): U; @delete op delete(...U): void; } @@ -1087,6 +1094,7 @@ worksFor(supportedVersions, ({ openApiFor }) => { id: uuid; } + #suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior" @patch(#{implicitOptionality: true}) op test(...Bar): Bar; `); @@ -1102,6 +1110,7 @@ worksFor(supportedVersions, ({ openApiFor }) => { id: string; } + #suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior" @patch(#{implicitOptionality: true}) op test(bar: Bar): void; model Foo { diff --git a/packages/rest/lib/resource.tsp b/packages/rest/lib/resource.tsp index 4031be092f5..472d928f299 100644 --- a/packages/rest/lib/resource.tsp +++ b/packages/rest/lib/resource.tsp @@ -132,6 +132,7 @@ interface ResourceCreateOrUpdate { @autoRoute @doc("Creates or update an instance of the resource.") @createsOrUpdatesResource(Resource) + #suppress "@typespec/http/deprecated-implicit-optionality" "for legacy behavior" @patch(#{ implicitOptionality: true }) // for legacy behavior createOrUpdate( ...ResourceParameters, @@ -188,6 +189,7 @@ interface ResourceUpdate { @autoRoute @doc("Updates an existing instance of the resource.") @updatesResource(Resource) + #suppress "@typespec/http/deprecated-implicit-optionality" "for legacy behavior" @patch(#{ implicitOptionality: true }) // for legacy behavior update( ...ResourceParameters, @@ -339,6 +341,7 @@ interface SingletonResourceUpdate, @@ -398,6 +401,7 @@ interface ExtensionResourceCreateOrUpdate, @@ -446,6 +450,7 @@ interface ExtensionResourceUpdate, diff --git a/website/src/content/docs/docs/libraries/http/reference/decorators.md b/website/src/content/docs/docs/libraries/http/reference/decorators.md index 921fdbfaef4..988fa1a6e50 100644 --- a/website/src/content/docs/docs/libraries/http/reference/decorators.md +++ b/website/src/content/docs/docs/libraries/http/reference/decorators.md @@ -298,11 +298,10 @@ Specify the HTTP verb for the target operation to be `PATCH`. @patch op update(pet: Pet): void; ``` +##### Using MergePatch template for proper merge-patch semantics + ```typespec -// Disable implicit optionality, making the body of the PATCH operation use the -// optionality as defined in the `Pet` model. -@patch(#{ implicitOptionality: false }) -op update(pet: Pet): void; +@patch op update(@body pet: MergePatchUpdate): void; ``` ### `@path` {#@TypeSpec.Http.path} From e0929eea7889b6cc10aca8876ef1229ab974b1f3 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 3 Mar 2026 08:06:56 -0800 Subject: [PATCH 2/4] Create replace-patch-implicit-optionality-warning-2026-2-3-15-59-37.md --- ...atch-implicit-optionality-warning-2026-2-3-15-59-37.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .chronus/changes/replace-patch-implicit-optionality-warning-2026-2-3-15-59-37.md diff --git a/.chronus/changes/replace-patch-implicit-optionality-warning-2026-2-3-15-59-37.md b/.chronus/changes/replace-patch-implicit-optionality-warning-2026-2-3-15-59-37.md new file mode 100644 index 00000000000..88e1ae0c035 --- /dev/null +++ b/.chronus/changes/replace-patch-implicit-optionality-warning-2026-2-3-15-59-37.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: deprecation +packages: + - "@typespec/http" +--- + +Deprecate use of `@patch(#{implicitOptionality: true})`. Use the explicit `MergePatch` for accurate json merge patch representation From 64cc65b71fc4d7c13edc574fa20c7a720f5eb6d0 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 3 Mar 2026 08:08:31 -0800 Subject: [PATCH 3/4] Create replace-patch-implicit-optionality-warning-2026-2-3-15-59-37-2.md --- ...h-implicit-optionality-warning-2026-2-3-15-59-37-2.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .chronus/changes/replace-patch-implicit-optionality-warning-2026-2-3-15-59-37-2.md diff --git a/.chronus/changes/replace-patch-implicit-optionality-warning-2026-2-3-15-59-37-2.md b/.chronus/changes/replace-patch-implicit-optionality-warning-2026-2-3-15-59-37-2.md new file mode 100644 index 00000000000..38a816ff0bc --- /dev/null +++ b/.chronus/changes/replace-patch-implicit-optionality-warning-2026-2-3-15-59-37-2.md @@ -0,0 +1,9 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: internal +packages: + - "@typespec/http-specs" + - "@typespec/rest" +--- + +Add suppressions for deprecated behavior From 404e3ad4f046613e93c988029f8287765bc4aff4 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Tue, 3 Mar 2026 11:18:16 -0500 Subject: [PATCH 4/4] format --- .../http-specs/specs/type/model/visibility/main.tsp | 2 +- packages/http/src/parameters.ts | 6 +----- packages/rest/lib/resource.tsp | 10 +++++----- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/http-specs/specs/type/model/visibility/main.tsp b/packages/http-specs/specs/type/model/visibility/main.tsp index bb4c63c652d..1a020005e49 100644 --- a/packages/http-specs/specs/type/model/visibility/main.tsp +++ b/packages/http-specs/specs/type/model/visibility/main.tsp @@ -88,6 +88,7 @@ op headModel(@bodyRoot input: VisibilityModel): OkResponse; @put op putModel(@body input: VisibilityModel): void; +#suppress "@typespec/http/deprecated-implicit-optionality" "for legacy test" @scenario @scenarioDoc(""" Generate abd send put model with write/update properties. @@ -98,7 +99,6 @@ op putModel(@body input: VisibilityModel): void; } ``` """) -#suppress "@typespec/http/deprecated-implicit-optionality" "for legacy test" @patch(#{ implicitOptionality: true }) op patchModel(@body input: VisibilityModel): void; diff --git a/packages/http/src/parameters.ts b/packages/http/src/parameters.ts index 9813f53aad0..f7ff6e9a342 100644 --- a/packages/http/src/parameters.ts +++ b/packages/http/src/parameters.ts @@ -5,11 +5,7 @@ import { Operation, Program, } from "@typespec/compiler"; -import { - getOperationVerb, - getPathOptions, - getQueryOptions, -} from "./decorators.js"; +import { getOperationVerb, getPathOptions, getQueryOptions } from "./decorators.js"; import { resolveRequestVisibility } from "./metadata.js"; import { HttpPayloadDisposition, resolveHttpPayload } from "./payload.js"; import { diff --git a/packages/rest/lib/resource.tsp b/packages/rest/lib/resource.tsp index 472d928f299..5df80acf6ec 100644 --- a/packages/rest/lib/resource.tsp +++ b/packages/rest/lib/resource.tsp @@ -129,10 +129,10 @@ interface ResourceCreateOrUpdate { * @template Resource The resource model to create or update. * @template Error The error response. */ + #suppress "@typespec/http/deprecated-implicit-optionality" "for legacy behavior" @autoRoute @doc("Creates or update an instance of the resource.") @createsOrUpdatesResource(Resource) - #suppress "@typespec/http/deprecated-implicit-optionality" "for legacy behavior" @patch(#{ implicitOptionality: true }) // for legacy behavior createOrUpdate( ...ResourceParameters, @@ -186,10 +186,10 @@ interface ResourceUpdate { * @template Resource The resource model to update. * @template Error The error response. */ + #suppress "@typespec/http/deprecated-implicit-optionality" "for legacy behavior" @autoRoute @doc("Updates an existing instance of the resource.") @updatesResource(Resource) - #suppress "@typespec/http/deprecated-implicit-optionality" "for legacy behavior" @patch(#{ implicitOptionality: true }) // for legacy behavior update( ...ResourceParameters, @@ -337,11 +337,11 @@ interface SingletonResourceUpdate, @@ -398,10 +398,10 @@ interface ExtensionResourceCreateOrUpdate, @@ -447,10 +447,10 @@ interface ExtensionResourceUpdate,