Describe the bug
The MergePatchUpdate doc comment says the transform follows RFC 7396, "applying the merge-patch transform recursively to keyed types" in the resource model. That recursion doesn't happen for a Record<T> field, which is a keyed type: the field itself is transformed, its values are emitted unchanged.
An optional record does become nullable, so the whole map can be cleared, but deleting a single key fails validation. The algorithm in RFC 7396 recurses into nested objects and treats null as a delete instruction at every level, so both of these are valid merge patches:
{"tags": null} // clear the whole map
{"tags": {"a": null}} // delete one key
Only the first validates. Per-key deletion cannot be expressed against the generated schema at all. Since the template also sets application/merge-patch+json as the request content type, that schema is what tells a client what the server accepts.
Versions: @typespec/compiler 1.14.0, @typespec/http 1.14.0, @typespec/openapi3 1.14.0.
Reproduction
import "@typespec/http";
using Http;
@service(#{ title: "Record repro" })
namespace RecordRepro;
model Widget {
name: string;
tags?: Record<string>;
}
model WidgetPatch is MergePatchUpdate<Widget>;
@route("/widgets/{id}")
@patch
op update(@path id: string, @body body: WidgetPatch): Widget;
Emitted WidgetPatch:
type: object
properties:
name:
type: string
tags:
anyOf:
- type: object
unevaluatedProperties:
type: string # <- no null branch
- type: 'null' # <- the field itself is clearable
Validating patch bodies against that schema:
VALID {"name":"x"}
VALID {"tags":null}
VALID {"tags":{"a":"b"}}
INVALID {"tags":{"a":null}}
Expected: the transform reaches the record's values, so unevaluatedProperties accepts T | null and the last case validates as "delete key a".
Describe the bug
The
MergePatchUpdatedoc comment says the transform follows RFC 7396, "applying the merge-patch transform recursively to keyed types" in the resource model. That recursion doesn't happen for aRecord<T>field, which is a keyed type: the field itself is transformed, its values are emitted unchanged.An optional record does become nullable, so the whole map can be cleared, but deleting a single key fails validation. The algorithm in RFC 7396 recurses into nested objects and treats
nullas a delete instruction at every level, so both of these are valid merge patches:{"tags": null} // clear the whole map {"tags": {"a": null}} // delete one keyOnly the first validates. Per-key deletion cannot be expressed against the generated schema at all. Since the template also sets
application/merge-patch+jsonas the request content type, that schema is what tells a client what the server accepts.Versions:
@typespec/compiler1.14.0,@typespec/http1.14.0,@typespec/openapi31.14.0.Reproduction
Emitted
WidgetPatch:Validating patch bodies against that schema:
Expected: the transform reaches the record's values, so
unevaluatedPropertiesacceptsT | nulland the last case validates as "delete keya".