NUMBER FORMAT
NUMBER FORMAT
Declare which separator convention a number field is presented in.
Pure presentation — no new field type, no new column, no DDL, no migration, and
no feature flag: the per-field opt-in is itself the switch.
The invariant
The store and the wire are always unformatted. The column stays NUMERIC
and the API keeps returning "1234.56" — dot decimal, no grouping — never
"1.234,56". This option is a hint to the client about which convention to
render in; the backend never formats and never parses a formatted string.
Note the value is a JSON string, not a JSON number, and always has been:
the driver returns NUMERIC as a string to preserve arbitrary precision (see
rollup-compute.service.ts,
which coerces it explicitly for the same reason) and
rowToDataRecord passes it
through untouched. Nothing here changes that — worth stating because a display
option is exactly where someone assumes otherwise.
That is what makes the option safe to flip on a live field: nothing about existing rows, filters, sorting, aggregation, export parsing or audit changes, so there is no migration and no window where a value can be misread.
It also mirrors the admin-level date_format preference, which is likewise
stored, published, and applied entirely by the client.
The option
// PATCH /object/field/{fieldSlug}
{ "options": { "min": 0, "number_format": "comma_decimal" } }| Value | 1234.56 renders as | Convention |
|---|---|---|
dot_decimal | 1,234.56 | US / UK |
comma_decimal | 1.234,56 | most of continental Europe |
| absent | client's own default | every field predating this option |
The values are named after the decimal separator, not a country: the decimal mark is what actually distinguishes the two, and "European" is not one convention (France uses a space for thousands).
Validation
NumberOptionsValidator rejects anything outside that list with a 400, rather
than ignoring it. An unrecognised value has to fail loudly — a client picks its
separator from this string, so a typo'd "european" would save cleanly and then
render in whatever the client falls back to, silently, on exactly the field
someone configured on purpose.
Absent stays valid, which is what keeps every pre-existing number field
working untouched.
Where it surfaces
number_format rides along in the field's options blob wherever field
metadata is already returned — the field detail endpoint, the fields[] block
on record reads, and public-form field metadata. No response DTO changed.
It is not in GET /object/field/manager: that grid deliberately excludes
raw options (SEC-03/14) and surfaces governance facts, not schema internals.
Scope
number only — NumberOptionsValidator owns the NUMBER slot in
FIELD_OPTIONS_VALIDATORS, and it is the only validator that checks this key.
currency and rating do not support it. Note what that means precisely:
the registry validates the keys a type knows about, it does not reject unknown
ones, so number_format set on any other type is stored and ignored rather
than 400'd — the same as any other stray key in an options blob. Nothing reads
it there. Don't rely on a rejection that will not come.
Client-side rendering, locale-aware input parsing, and per-viewer overrides are out of scope. Not formatting server-side is what keeps the round-trip lossless: a value that leaves as a number comes back as the same number.
Related
Sorting a number field treats an empty value as the lowest value — see
the records query builder. Blanks lead
ASC and trail DESC instead of Postgres' default of ranking NULL highest.