A Choice value your field does not know is stored anyway — so a drifted vocabulary never announces itself
Writing a value that is not in a Choice column's Choices does not fail: REST stores it verbatim (201 on create, 204 on MERGE, and even the Validate… endpoints accept it). So when the values your code can write and the values the field declares drift apart, nothing at runtime tells you — the only place that can catch it is a build-time check of the two lists against each other.
Zápis hodnoty, která v Choices Choice sloupce není, neselže: REST ji uloží doslova (201 při založení, 204 u MERGE a přijmou ji i endpointy Validate…). Když se tedy rozejdou hodnoty, které umí zapsat kód, a hodnoty, které deklaruje sloupec, za běhu se to nedozvíš nikde – zachytit to umí jen kontrola obou seznamů při buildu.
Correction (2026-09-24). The first version of this article said the opposite: that such a write fails with HTTP 400 and a best-effort logger swallows it, so audit entries silently never exist. That was inferred from an older note, not measured. A live test on SharePoint Online showed the write succeeds — the measurements are in Choice fields accept any value over REST. The rules below survive the correction; the reason for them changed.
Symptom
The values your code can write and the values the column declares have drifted apart, and nothing complains:
// code
export type AuditAction = 'create' | 'delete' | 'archive' | /* ... */ | 'offboard' | 'merge';
// provisioning manifest
{ internalName: 'Action', type: 'Choice', choices: ['create', 'delete', 'archive' /* ...and no more */] }
Writes of offboard and merge succeed. The items exist, carrying values the field definition does not list. Anything built from the definition rather than from the data — the dropdown in a form, a filter or picker generated from Choices, a reconciler that widens Choices from the manifest — does not know those values.
Cause
REST does not check Choice values against Choices, and FillInChoice makes no difference, so there is no runtime error to see — or to swallow. The type-checker cannot help either: both sides are valid on their own, and nothing in the language ties a union type to a literal array in another file.
A reconciler that widens Choices on already-deployed sites does not close the gap, because its source of truth is the same manifest where the values are missing.
Rules
- Make the contract machine-checked. Both sides are literals, so a small build script can parse them and compare. Fail the build on a mismatch, and check the human-readable labels in the same pass.
- Where runtime stays silent, the build has to speak. SharePoint accepts the value, and a best-effort writer would swallow an error anyway — the drift can only be found before runtime.
- Widening a Choice column needs a schema-version bump (or whatever re-runs your reconcile). Existing sites reconcile only when the provisioning version changes; without the bump, the new value stays missing from the definition everywhere the column is already deployed.
- Scope the "already reconciled" marker per site.
localStorageis shared across the whole origin, so a key without the site URL marks the work done after the first site and never runs on the second. - Test the guard by breaking it. Remove one value in a scratch copy and confirm the script exits non-zero — a guard that has never failed has never been shown to work.
- Don't explain a failed write with the vocabulary. If a write really fails, look at the body format, a missing column or the headers — an unknown Choice value is not the cause.
Quick check
# Values the code can produce vs. values the field declares
grep -oE "'[a-z-]+'" src/**/AuditService.ts | sort -u
grep -A2 "internalName: 'Action'" src/**/Provisioning*.ts
Notes
- Choice fields accept any value over REST — the measurements:
POST,MERGE,ValidateUpdateListItemandAddValidateUpdateItemUsingPathall store an unknown value verbatim. - A new Choice value in your provisioning code never reaches already-deployed sites — how to reconcile the definition without narrowing it.