microsoft / microsoft/typespec

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

Open
#11,533 3 comments 1 reaction 0 assignees View on GitHub
bug lib:http triaged:core
Dominant language
Java
Stars
5.9k
Forks
394
Avg merge
1d 23h
Merged PRs (30d)
104

Description

### Describe the bug

The `MergePatchUpdate` doc comment says the transform follows [RFC 7396](https://www.rfc-editor.org/rfc/rfc7396), "applying the merge-patch transform recursively to keyed types" in the resource model. That recursion doesn't happen for a `Record` 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](https://www.rfc-editor.org/rfc/rfc7396) recurses into nested objects and treats `null` as a delete instruction at every level, so both of these are valid merge patches:

```json
{"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

```tsp
import "@typespec/http";

using Http;

@service(#{ title: "Record repro" })
namespace RecordRepro;

model Widget {
name: string;
tags?: Record;
}

model WidgetPatch is MergePatchUpdate;

@route("/widgets/{id}")
@patch
op update(@path id: string, @body body: WidgetPatch): Widget;
```

Emitted `WidgetPatch`:

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

Contributor guide

Open the contributing guide

Research direction

Start with the MergePatchUpdate transform and run the RecordRepro example from the issue using the listed TypeSpec packages. Inspect the generated WidgetPatch schema and validate the four patch bodies shown. Done means the schema accepts {"tags":{"a":null}} while preserving whole-map clearing and non-null record values.

Written by the indexing model from the issue text.

Assessment

Tech stack
openapi
Domain
api, compilers
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
65/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.