|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +"@objectstack/rest": minor |
| 4 | +"@objectstack/objectql": minor |
| 5 | +--- |
| 6 | + |
| 7 | +feat(spec,rest,objectql)!: a closed field-level error catalog, and Zod stops leaking onto the wire (#3977) |
| 8 | + |
| 9 | +Settles the vocabulary ADR-0112 D6 deferred, per [ADR-0114](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0114-field-level-error-code-catalog.md). |
| 10 | + |
| 11 | +**`FieldErrorCode` — a closed, lowercase catalog.** 27 members covering what the |
| 12 | +six emitters already emit. `FieldErrorSchema.code` tightens from `z.string()` to |
| 13 | +this enum, so a validation body's per-field codes are validated for the first time. |
| 14 | +`FieldValidationError.code` (objectql) and `FieldCoerceError.code` (rest) stop |
| 15 | +being a hand-listed union and a bare `string` respectively and reference the |
| 16 | +catalog, so the three cannot drift apart. |
| 17 | + |
| 18 | +Lowercase is deliberate, not an oversight against ADR-0112's SCREAMING_SNAKE: a |
| 19 | +top-level code names the condition the *request* hit, while a field-level code |
| 20 | +names the *constraint* the value violated — and constraints are declared in the |
| 21 | +metadata's own snake_case, so `max_length` the code and `max_length: 50` the |
| 22 | +property are the same word on purpose. |
| 23 | + |
| 24 | +**Zod issue codes no longer reach the wire (wire-visible).** Routes that validate |
| 25 | +with Zod passed its vocabulary straight through, so `fields[]` spoke a different |
| 26 | +language depending on which route served it, and `too_small` was ambiguous between |
| 27 | +a short string, a small number and a short array. `zodIssuesToFields` now maps |
| 28 | +using Zod's `origin`/`format`: |
| 29 | + |
| 30 | +| Was | Now | |
| 31 | +|:---|:---| |
| 32 | +| `too_small` | `min_length` / `min_value` / `min_items` | |
| 33 | +| `too_big` | `max_length` / `max_value` / `max_items` | |
| 34 | +| `invalid_format` | `invalid_email` / `invalid_url` / `invalid_format` | |
| 35 | +| `invalid_value` | `invalid_option` | |
| 36 | +| `unrecognized_keys` | `unknown_field` | |
| 37 | +| `invalid_union`, `invalid_element`, `invalid_key` | `invalid_shape` | |
| 38 | + |
| 39 | +**A missing required property now reports `required`, not `invalid_type`.** Zod |
| 40 | +spells "absent" as a type mismatch against `undefined`, so passing it through made |
| 41 | +a form mark a *missing* input as the wrong *type*. The two are indistinguishable on |
| 42 | +the issue alone, so the mapper takes the parsed input as an optional argument and |
| 43 | +walks the issue path; a caller that cannot supply it keeps `invalid_type` rather |
| 44 | +than guessing. |
| 45 | + |
| 46 | +**`unknown_param` → `unknown_field`.** `ActionParamIssue.code` references the |
| 47 | +catalog instead of its own literal union; the `param` key beside it already says |
| 48 | +what was addressed. |
| 49 | + |
| 50 | +**Not changed:** `EnhancedApiErrorSchema.fieldErrors` keeps its name even though |
| 51 | +every producer emits `fields`. Retiring an authorable key needs a tombstone plus a |
| 52 | +migration (ADR-0104's contract guard), so it lands on its own — the property now |
| 53 | +carries a banner saying which name the wire uses. |
0 commit comments