Skip to content

[Bug]: MergePatchUpdate omits the null branch on Record values, so per-key deletion fails validation #11533

Description

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".

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions