BLUF
The 10DLC design returns API responses as map[string]any specifically so that production fields absent from the published spec are not silently dropped. Read-modify-write updates reintroduce exactly that loss at write time — and because both brand and campaign PUT are full replacements, the dropped fields get nulled server-side. This needs a deliberate decision before the first update command is written.
Background
Production returns fields the OpenAPI spec does not document — attMessageClass and subId on campaigns, universalEin and referenceId on brands. Go's encoding/json silently discards unknown fields when decoding into a struct, so the foundation PR made responses lossless map[string]any rather than typed structs.
That protects reads. It does nothing for writes.
The problem
Both PUT /brands/{brandId} and PUT /campaigns/{campaignId} state verbatim:
This is a full replacement — omitted optional fields will be set to null.
So update must be read-modify-write: GET current, overlay changed fields, PUT the complete result. The design already specifies a typed response→request mapper for that step, because response and request field names differ in case (subscriberOptIn in responses vs subscriberOptin in requests).
But a typed request struct can only carry fields the CLI models. Any production field it doesn't know about is absent from the PUT body — and the API nulls it.
Concretely: a customer's brand carries referenceId. The CLI's typed brand-update struct has no referenceId. A user runs band tendlc brand update B0IRNU4 --email new@example.com. The mapper builds a request from the fields it knows, the PUT replaces the brand, and referenceId is gone.
This is the failure the lossless decision was meant to prevent, moved from decode-time to write-time — and worse, because it destroys customer data instead of hiding it from output.
Two viable resolutions
Mutate the read map in place. GET into map[string]any, apply changed fields to that map, PUT it back. Lossless end to end. Cost: no typed validation on the update path, and the response→request case mismatch (subscriberOptIn → subscriberOptin) has to be handled as an explicit key-rename step on the map.
Typed mapper that round-trips unknown keys. Keep typed requests for validation, but have the mapper carry an Extra map[string]json.RawMessage of everything it didn't recognize and re-emit it on write. Cost: custom marshalling, and a test proving unknown keys survive a full RMW cycle.
I lean toward the second — validation on a 20-plus-field payload with conditional per-brandType requirements is worth keeping — but either is defensible. The unacceptable outcome is deciding this implicitly.
Acceptance
Whichever is chosen, a test must prove it: seed a response containing a field the CLI does not model, run a full read-modify-write cycle, and assert the field is present and unchanged in the outgoing PUT body.
Related
- Design:
docs/specs/2026-08-12-tendlc-direct-registration-center-design.md, "Mutating semantics" and "Design findings carried forward from PR 1's review"
- Blocks: the
update commands in PR 2 (customer profiles) and PR 4 (brands)
- Found during the final review of PR 1 (foundation), which itself contains no write operations.
BLUF
The 10DLC design returns API responses as
map[string]anyspecifically so that production fields absent from the published spec are not silently dropped. Read-modify-write updates reintroduce exactly that loss at write time — and because both brand and campaignPUTare full replacements, the dropped fields get nulled server-side. This needs a deliberate decision before the first update command is written.Background
Production returns fields the OpenAPI spec does not document —
attMessageClassandsubIdon campaigns,universalEinandreferenceIdon brands. Go'sencoding/jsonsilently discards unknown fields when decoding into a struct, so the foundation PR made responses losslessmap[string]anyrather than typed structs.That protects reads. It does nothing for writes.
The problem
Both
PUT /brands/{brandId}andPUT /campaigns/{campaignId}state verbatim:So
updatemust be read-modify-write: GET current, overlay changed fields, PUT the complete result. The design already specifies a typed response→request mapper for that step, because response and request field names differ in case (subscriberOptInin responses vssubscriberOptinin requests).But a typed request struct can only carry fields the CLI models. Any production field it doesn't know about is absent from the PUT body — and the API nulls it.
Concretely: a customer's brand carries
referenceId. The CLI's typed brand-update struct has noreferenceId. A user runsband tendlc brand update B0IRNU4 --email new@example.com. The mapper builds a request from the fields it knows, the PUT replaces the brand, andreferenceIdis gone.This is the failure the lossless decision was meant to prevent, moved from decode-time to write-time — and worse, because it destroys customer data instead of hiding it from output.
Two viable resolutions
Mutate the read map in place. GET into
map[string]any, apply changed fields to that map, PUT it back. Lossless end to end. Cost: no typed validation on the update path, and the response→request case mismatch (subscriberOptIn→subscriberOptin) has to be handled as an explicit key-rename step on the map.Typed mapper that round-trips unknown keys. Keep typed requests for validation, but have the mapper carry an
Extra map[string]json.RawMessageof everything it didn't recognize and re-emit it on write. Cost: custom marshalling, and a test proving unknown keys survive a full RMW cycle.I lean toward the second — validation on a 20-plus-field payload with conditional per-
brandTyperequirements is worth keeping — but either is defensible. The unacceptable outcome is deciding this implicitly.Acceptance
Whichever is chosen, a test must prove it: seed a response containing a field the CLI does not model, run a full read-modify-write cycle, and assert the field is present and unchanged in the outgoing PUT body.
Related
docs/specs/2026-08-12-tendlc-direct-registration-center-design.md, "Mutating semantics" and "Design findings carried forward from PR 1's review"updatecommands in PR 2 (customer profiles) and PR 4 (brands)