b0ab0dd5dd
Create/update operations were sending empty request bodies to Paperless,
producing two visible failure modes:
1. POST creates (correspondents, tags, document types, storage paths,
custom fields) returned `400 {"name":["This field is required."]}`
even when the caller passed a valid name.
2. PATCH updates (documents, correspondents, tags, etc.) returned 200
and bumped the row's `modified` timestamp, but no fields actually
changed — server-side this looked like a successful no-op PATCH.
3. Bulk edit operations (`api/documents/bulk_edit/`,
`api/bulk_edit_objects/`) failed because the inner `parameters` field
went out as `{}`, so add_tag/remove_tag/etc. arrived without a tag id.
Reproducer: `correspondents.create(name="ACME Corp")` against a real
Paperless instance returns 400 with the body above.
Root causes are two related serialization patterns where the
compile-time type of the value to serialize is `object`:
- `PostWithResultAsync` / `PatchWithResultAsync` accept the request as
`object` and pass it to `PostAsJsonAsync<TValue>` /
`JsonContent.Create<T>(value, ...)`. The generic `T` is inferred as
`object`, and through the configured DI HttpClient pipeline
(DelegatingHandler + Polly retry policy) the body that reaches the
wire is empty, even though the same JsonContent's
`ReadAsStringAsync()` returns the expected JSON.
- `BulkEditDocumentsAsync` / `BulkEditObjectsAsync` wrap the call args
in an anonymous type whose `parameters` field has compile-time type
`object?`. System.Text.Json serializes that property against
`object`, producing `"parameters":{}` regardless of the runtime value.
Fix: serialize against the runtime type explicitly and materialize the
JSON into a `StringContent` before the request leaves this client.
For bulk edits, build the body via `JsonObject` and serialize the
`parameters` payload against its runtime type.
Adds wire-format pinning tests for create / update / bulk_edit /
bulk_edit_objects that capture the outbound request body and assert the
expected payload shape. These do not reproduce the original bug under
MockHttp (the test helper bypasses the DI handler chain), but they pin
the new serialization for future refactors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>