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 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 diff --git a/packages/http-specs/specs/type/model/visibility/main.tsp b/packages/http-specs/specs/type/model/visibility/main.tsp index 80be39ab019..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. 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..f7ff6e9a342 100644 --- a/packages/http/src/parameters.ts +++ b/packages/http/src/parameters.ts @@ -5,14 +5,7 @@ import { Operation, Program, } 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 { getOperationVerb, getPathOptions, getQueryOptions } from "./decorators.js"; import { resolveRequestVisibility } from "./metadata.js"; import { HttpPayloadDisposition, resolveHttpPayload } from "./payload.js"; import { @@ -123,23 +116,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..5df80acf6ec 100644 --- a/packages/rest/lib/resource.tsp +++ b/packages/rest/lib/resource.tsp @@ -129,6 +129,7 @@ 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) @@ -185,6 +186,7 @@ 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) @@ -335,6 +337,7 @@ interface SingletonResourceUpdate): void; ``` ### `@path` {#@TypeSpec.Http.path}