Skip to content

A number field's declared scale is never enforced — values with more decimals are accepted and stored verbatim (min/max on the same field are enforced) #7501

Description

@baozhoutao

Summary

A number field that declares scale is not validated against it at runtime — a value with more decimal places than scale allows is accepted and stored verbatim. There is no error, no rounding, no warning.

Measured on @objectstack/*@17.0.0-rc.6, driver-sql (better-sqlite3), via both the REST create endpoint and the CSV import endpoint.

Repro

Field declaration:

work_hours: {
  type: 'number',
  label: 'Max hours per shift',
  precision: 5,
  scale: 0,        // ← declares "integer, no decimals"
  defaultValue: 12,
  min: 1,
  max: 12,
}

POST /api/v1/data/<object> with { "work_hours": 11.5 }

  • Expected: rejected (400, an INVALID_* envelope), or at minimum rounded to the declared scale.
  • Actual: HTTP 201, and the stored value is 11.5 — verified by reading the row straight out of storage, not through the API.

Same result with scale: 1 and a two-decimal input, and same result through
POST /api/v1/data/<object>/import (format: "csv").

min / max on the same field are enforced (-1400, code: "min_value"), so the validator does run — it just has no branch for scale.

Where it comes from

record-validator's number branch only tests def.min and def.max. A full search of
objectql / runtime / cli for scale finds it only in @objectstack/spec as descriptive
metadata ("Decimal places") — there is no runtime consumer anywhere in the shipped packages.

So scale today is documentation, not a constraint. That is surprising given it sits next to
precision, min and max in the same field declaration, all of which read as constraints.

Why it matters

A downstream app declared scale: 0 to express "this must be a whole number" and shipped it,
reasonably assuming the declaration was enforced the way min/max are. It is not — decimals
flow through to storage. There is no way to express "integer" in the field contract at all today,
so every such rule has to be re-implemented in an application hook, one field at a time.

Ask

Either of these would close it:

  1. Enforce scale in the number validator (reject, or round to the declared scale — either is
    predictable, silence is not), consistent with how min/max already behave; or
  2. If scale is intentionally display-only, say so in the spec description (it currently reads
    just "Decimal places", which does not distinguish "stored with" from "rendered with"), and
    provide a real way to declare an integer constraint.

Secondary observation (same field, same request path)

The min/max rejection message is half-translated: the field label is localized but the
sentence template is not, producing e.g.

Max hours per shift must be ≥ 1

in a zh-CN environment. It reaches end users verbatim on the import path, so an app cannot present
a fully localized message without intercepting and rewriting it. Mentioning it here rather than
opening a second card since it is the same validator; happy to split it out if preferred.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions