Write semantics¶
How PUT actually behaves when you modify a record. Read this before you write anything back to
Sierra — it's where the costly surprises live.
PUT phones is additive, not replacement¶
Behavior: Including a top-level phones array in a PUT body appends those phones to the
existing list instead of replacing it — the opposite of how most fields behave. You get duplicate
phone entries.
Type: Bug-or-quirk.
How to handle: Don't send the top-level phones key at all. Manage phone data through
varFields (phone field tags) instead, which follow normal replacement rules.
How we know: A pilot batch returned 204 (success) on every record, yet verification showed
every record had gained duplicate phone entries. Removing the phones key from the payload fixed it.
PUT varFields replaces per fieldTag group¶
Behavior: A PUT containing varFields is neither a whole-array replace nor a plain merge
— it replaces one fieldTag group at a time. For every fieldTag present in your payload, Sierra
rewrites that tag's entire group to exactly the fields you sent; a fieldTag you send no members
of is left untouched. Two consequences that look contradictory until you see the rule:
- Send one of a tag's two fields (drop the other) → the dropped sibling is deleted.
- Send none of a tag → every field of that tag survives — which is why "just omit it from the array" silently does nothing when the field is the only one of its tag.
Type: By design.
Deleting a field¶
Two routes, depending on the field:
- It shares its
fieldTagwith fields you're keeping — send those, omit the target. The group is rewritten without it, so it's removed. Works on patron, item, and bib. - It's the only field of its tag (or you want the whole tag gone) — omission can't do it
(sending none = untouched). Send the field with empty
contentinstead. What that does is record-type-specific:
| Field | content: "" |
subfields: [] |
subfields blanked | content: null |
|---|---|---|---|---|
| Patron · content | removed | — | — | 400 |
| Item / Bib · content | blanked to an empty shell (row stays) | — | — | 400 |
| Item / Bib · subfields | 400 | 400 | blanked to an empty shell | 400 |
So a patron content varField is the one field with a clean full removal by blanking. For item and bib fields — and any subfield-bearing field — blanking only leaves an empty shell; the only clean removal is route 1 (drop one of a repeated tag). This matches Innovative treating standalone item/bib varField deletion as an enhancement request.
Editing an existing field: GET-modify-PUT — fetch fields=,, change the field in place, PUT
back. Keeping each field's identity is what makes the group-replace update it rather than
duplicate it. See Safely edit a record.
# Delete a patron varField by blanking its content (patrons drop empty-content entries):
resp = client.request("GET", f"patrons/{record_num}", params={"fields": ","})
varfields = resp.json().get("varFields", [])
for vf in varfields:
if vf.get("fieldTag") == "x" and is_target(vf):
vf["content"] = "" # empty STRING, not None (null -> 400); patron drops the entry
client.request("PUT", f"patrons/{record_num}", json={"varFields": varfields})
# Delete ONE of several same-tag fields on ANY record type: send the keepers, omit the target.
keep = [vf for vf in varfields if not is_target(vf)] # target shares its fieldTag with a keeper
client.request("PUT", f"items/{record_num}", json={"varFields": keep}) # target's tag-group rewritten w/o it
# On an item/bib, blanking only EMPTIES the field — the row stays (NOT a removal):
after = client.request("GET", f"items/{record_num}", params={"fields": ","}).json()["varFields"]
[vf for vf in after if vf.get("fieldTag") == "x"] # -> [{"fieldTag": "x", "content": "", ...}]
How we know: Re-derived on sierra-test 2026-07-23 with a reversible probe
(scripts/probe-varfield-write-semantics.py) that first proves each PUT mutates the record (a
positive control), so a "survived" result can't be a silent no-op. The per-fieldTag-group rule
was confirmed on patron, item, and bib (drop one of a repeated tag → the sibling is deleted;
drop the whole tag → all survive). The deletion table was measured for content and subfield
fields on item and bib, and for content fields on patron (patron varFields carry only
content — no subfield-bearing patron field was found to test). Each cell was measured on one
representative field tag per record type; we assume Sierra treats tags uniformly within a record
type, but that dimension was not swept. An earlier draft of this page called the behaviour "full
replacement", then "merge" — both were partial views of the per-tag-group rule. Behaviour is
deployment- and version-specific; re-run the probe on your own system before relying on it —
see Verify write semantics on your deployment.
A successful PUT returns 204, not 200¶
Behavior: A successful update returns HTTP 204 No Content. There is no response body.
Type: By design (standard REST, but easy to miss).
How to handle: Check for 204. Don't call .json() on the response — there's nothing to parse.
resp = client.request("PUT", f"patrons/{record_num}", json=patch)
if resp.status_code == 204:
... # success, no body
How we know: Every successful patron update across tens of thousands of records returned 204.
fixedFields PUT requires label beside value¶
Behavior: A fixedFields entry in a PUT body must include both label and value. Sending
just value makes Sierra reject the entire payload with 400 Invalid JSON : field(s) unknown :
fixedFields.
Type: By design (asymmetric: GET responses always include label, so it's easy to assume
value-only works on write).
How to handle: Include the canonical label string for each field:
# Rejected (400):
{"fixedFields": {"268": {"value": "p"}}}
# Accepted (204):
{"fixedFields": {"268": {"label": "Notice Preference", "value": "p"}}}
Some fields also have a top-level convenience key (e.g. manual block via blockInfo); others, like
notice preference, only work through fixedFields.
How we know: An entire batch failed with thousands of identical 400s until the label key was
added; the same payload then returned 204.
PUT rejects ~9 read-only top-level fields¶
Behavior: Several fields returned on GET are rejected on PUT. A naïve "PUT back what I
GETted" fails with 400 Invalid JSON : field(s) unknown : ... listing fields such as id,
homeLibrary, autoBlockInfo, message, moneyOwed, suppressed, updatedDate, deleted,
createdDate.
Type: By design.
How to handle: Send only the mutable subset you actually want to change. For varField edits,
{"varFields": [...]} alone is sufficient — don't echo the whole GET response back.
How we know: Surfaced building a rollback that PUT a full GET response verbatim; Sierra named the
unknown fields in the 400.
Empty-content varFields: dropped on patrons, blanked on items and bibs¶
Behavior: An entry sent with empty content behaves by record type on save (the PUT
returns 204 either way):
- Patron: Sierra removes the entry entirely — the patron deletion lever (see PUT varFields replaces per fieldTag group above).
- Item and bib: the entry is kept with empty content (an empty shell); it is not removed.
Type: By design on patrons (it is the deletion path); the patron-vs-item/bib split is a quirk.
How to handle: If you don't intend a deletion, pre-filter empty-content varFields before
sending (if vf.get("content"):) so you don't blank a patron field by accident. In before/after
verification, treat "an empty patron varField disappeared" as expected.
How we know: On sierra-test (2026-07-23), content: "" removed the field on every patron tested
and left an empty shell on every item and bib tested.
emails/phones/addresses/names are derived projections¶
Behavior: These four top-level fields are not independent state — Sierra renders them from
the underlying varFields and re-renders them after every varFields PUT.
Type: By design.
How to handle: Don't compare these top-level arrays before/after a write to detect "did something else change?" — they shift on every legitimate varField edit. Compare the underlying varFields instead. See Why writes have side effects for the underlying reason.
How we know: Equality checks on these arrays false-positived on every successful varField edit during pilot runs; switching the check to the underlying varFields resolved it.