A model that silently adjusts your input has answered a question you did not ask. Worthune models refuse instead — and the refusal arrives structured enough to turn into good UX.
Every input to every model has a published domain, and a request outside it is rejected — never clamped, never rounded into range, never quietly substituted. Ask the FIRE model about a 150-year-old and the response is not a projection for a 65-year-old with a warning buried somewhere. It is ok: false and a structured error naming the field and the rule: currentAge must be between 18 and 65. This piece is about what a front end should do with that.
The error contract
Failures return the model name and an errors array. Each error carries a message and, for field-level problems, the field it belongs to. Missing fields are each named individually — omit five inputs and you get five errors, not one vague complaint. Unknown fields are rejected too, which catches typos in integration code the day you write it instead of the day a user notices a field being ignored.
{ "ok": false, "model": "fire", "errors": [ { "field": "currentAge", "message": "must be between 18 and 65" } ] }Every message is written to be renderable: field plus constraint. The machine-readable contract at GET /api/v1/models/{model} carries the same domains, so a client can validate before calling and treat the server rejection as the backstop, not the primary UX.
Validate-first, reject-as-backstop
The right architecture uses the contract twice. At form-render time, fetch the contract and bake the domains into the controls: a slider that stops at 65, a numeric field with min and max, a select with the actual choices. Most users then never see a rejection, because the form cannot express one. At submit time, the server's validation is the backstop for whatever slipped through — a stale cached contract, a programmatic caller, a race. Map each returned field error to its control, verbatim message included; the messages are already user-grade.
- Fetch the contract and constrain controls to the published domains.
- Map server field errors to controls one-to-one — the field property is the join key.
- Render messages verbatim, formatting decimal ranges as percentages where your control does; they were written for humans.
- Never resubmit an adjusted value the user did not choose.
- Log rejections with their fields — they are your form-design telemetry.
The design temptation to resist
When a user types an out-of-range value, the tempting fix is to clamp it client-side — snap 150 down to 65 and proceed. That reproduces exactly the silent-adjustment behavior the API refuses to perform, one layer up. The user asked about 150; showing them an answer about 65 without their consent is the answer to a different question wearing the same chrome. The graceful pattern is to hold the value, mark the field, state the range, and let the user choose — including choosing to understand that the tool does not model their case. For products in regulated flows, that distinction has a name in the compliance literature: an adjusted input that influences a financial decision without the user's knowledge is the kind of silent behavior examiners ask pointed questions about.
Copy patterns that work
For range errors, state the range and the reason it exists when the spec gives one: withdrawal rates run 2 to 6 percent because the model prices the studied range, not fantasy. For cross-field rules — a retirement age that must exceed current age, an ARM period that must end at least a year before payoff — the API's message names the rule, along with the model's other constraint notes when it has several; render it as a relationship between the two fields, visually tying them. For out-of-scope questions (a user who wants a housing-crash scenario in a model whose appreciation domain starts at zero), the honest copy is that the tool does not model that case, ideally with a pointer to what it does model. Users forgive scope. They do not forgive discovering that a number was quietly not about their situation.
The audit angle
Rejection-first validation has a second constituency: reviewers. A tool that clamps has to explain, for every historical answer, whether the inputs shown were the inputs used. A tool that rejects can make a one-sentence representation — every stored answer was computed from exactly the inputs in its envelope, because no other path exists — and the envelope's echoed inputs prove it per answer. The UX decision and the audit posture are the same decision.
Sources
- [1] Worthune API documentation (validation and error shapes). https://worthune.com/docs
- [2] Worthune writing: The Glass Box Principle. https://worthune.com/writing/glass-box-principle