fix: coerce empty maps to objects in patch payloads - #31
Conversation
jsonPatch() and jsonMergePatch() (and their status variants) encoded
payloads with a raw json_encode, while update() routes through
toJsonPayload() which coerces empty PHP arrays back to JSON {} objects.
Structures fetched from the apiserver decode {} to PHP [], so any patch
built from fetched data re-encoded empty maps as [] and the apiserver
rejected it with 422 - e.g. a StatefulSet template's emptyDir: {}
volumes, which every stored template carries when medium is unset.
Patch payloads now build through toJsonPatchPayload() and
toJsonMergePatchPayload() on K8sResource, sharing the same structural
coercion as full payloads. A JSON Patch operation value that is itself
an empty array stays a list, so clearing list fields such as finalizers
keeps working. The exemption list gains finalizers and conditions: both
are list-typed in every Kubernetes API, and the library's own finalizer
clearing and status patches rely on them staying [] when the merge
document is coerced.
This also fixes jsonPatchStatus() silently dropping array input: it
wrapped the array in new JsonPatch($patch), but JsonPatch has no
constructor, so the operations were discarded and [] was sent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile SummaryThe PR routes JSON Patch and JSON Merge Patch requests through structural payload coercion, preserves known empty list fields, and fixes array input handling for status patches. It adds focused payload and integration coverage plus documentation for empty-map behavior. Confidence Score: 4/5The direct replacement of an empty map remains broken and should be corrected before merging. The new serializer fixes nested empty maps but unconditionally preserves an exact empty operation value as Files Needing Attention: src/Kinds/K8sResource.php, tests/PatchPayloadTest.php Important Files Changed
Prompt To Fix All With AI### Issue 1
src/Kinds/K8sResource.php:176-178
**Empty map values remain arrays**
When a JSON Patch operation clears a map-valued field such as `/metadata/labels` using `value => []`, this guard skips coercion and serializes the value as `[]`, causing Kubernetes to reject the patch because the field requires `{}`.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "fix: coerce empty maps to objects in pat..." | Re-trigger Greptile |
| if (is_array($value) && $value !== []) { | ||
| $operations[$index]['value'] = $this->coerceEmptyArraysToObjects($value); | ||
| } |
There was a problem hiding this comment.
Empty map values remain arrays
When a JSON Patch operation clears a map-valued field such as /metadata/labels using value => [], this guard skips coercion and serializes the value as [], causing Kubernetes to reject the patch because the field requires {}.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Kinds/K8sResource.php
Line: 176-178
Comment:
**Empty map values remain arrays**
When a JSON Patch operation clears a map-valued field such as `/metadata/labels` using `value => []`, this guard skips coercion and serializes the value as `[]`, causing Kubernetes to reject the patch because the field requires `{}`.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
The bug
jsonPatch()andjsonMergePatch()(and their status variants) encoded array payloads with a rawjson_encode, whileupdate()routes throughtoJsonPayload()which coerces empty PHP arrays back to JSON{}objects (#25).Structures fetched from the apiserver decode
{}to PHP[], so any patch payload built from fetched data re-encoded empty maps as[]— and the apiserver rejects that with 422. The canonical trigger is a StatefulSet/Deployment template'semptyDir: {}volumes: every stored template wheremediumis unset carries them (Goomitemptydrops the medium key but keeps the{}struct), so a reconcile loop that fetches the template, modifies containers, and JSON-Patches it back fails on every tick.The fix
Patch payloads now build through
K8sResource::toJsonPatchPayload()/toJsonMergePatchPayload(), sharing the same structural coercion as full payloads:valueis coerced when it is a non-empty array. A value that is itself an empty array stays a list, so clearing list fields keeps working:['op' => 'replace', 'path' => '/metadata/finalizers', 'value' => []]still encodes"value":[].{}(a merge patch document is always a JSON object;[]would replace the entire target).$emptyArrayListsexemptions apply at any depth, exactly as intoJsonPayload(). The list gainsfinalizersandconditions— both are list-typed in every Kubernetes API, and the library's own finalizer clearing (jsonMergePatch(['metadata' => ['finalizers' => []]]), seeFinalizerTest) and status patches (jsonMergePatchStatus(['status' => ['conditions' => []]]), seeStatusSubresourceTest) rely on them staying[]once merge documents are coerced. This also fixesupdate()sendingfinalizers: {}when the last finalizer is removed.Also fixed in passing:
jsonPatchStatus()silently discarded array input — it wrapped the array innew JsonPatch($patch), butJsonPatchhas no constructor, so an empty[]patch was sent instead of the given operations.Tests
tests/PatchPayloadTest.phppins the encoded payload bytes for both patch flavors and both input types (array and patch object):'emptyDir' => []must encode as"emptyDir":{}, top-level empty op values stay[], exempted list fields stay[]at depth, strings containing?: []are never touched, empty JSON Patch stays[], empty merge patch becomes{}. The coercion assertions were written first and seen red against the raw-json_encodebehavior (7 failures), then green with the fix.tests/PatchIntegrationTest.phpgains a live regression test that fetches a Deployment whoseemptyDir: {}decoded to[]and JSON-Patches the template back — the exact fetch-modify-patch loop that previously 422'd.Unit suites pass; Psalm reports the same 571 baseline issues before and after (no new); Pint clean; docs build clean.
Local verification
422 Unprocessable Entitywhen run against the pre-fix source and passes with the fix.🤖 Generated with Claude Code