The output document
A form produces a document. The output block says what that document
contains.
form loan version = "1.0" {
field amount { type = number }
field term { type = integer }
field monthly { type = number value = pmt(0.05 / 12, term, amount) }
output {
amount: number
term: integer
monthly: number
}
}
The output block is a schema
Each entry names a path and the type of the value at that path:
output {
applicant.first_name: string
applicant.income: number
dependants: array of object { name: string, age: integer }
employment: enum("employed", "self")
}
Notice what is not there: you do not write amount: amount. You write the
path and the type, and the runtime projects the data plane onto it.
That is what the specification means by an output schema. The output block is
a promise about the shape of the document, and the runtime checks that the
promise is kept — a field that produces a string where the schema says
number is an error, not a surprise at the far end.
The output document is JSON
The engine produces a value tree, and the serialiser writes it as JSON. Answer
the loan form above with amount=25000 and term=60:
{ "amount": "25000", "term": "60", "monthly": "471.780841100273321124203029223732" }
One detail about it is deliberate and worth knowing.
The numbers are JSON strings. A JSON number cannot carry a canonical decimal without passing through binary floating point, and the specification says a serialiser that cannot do that must emit a string. So it does. The conformance rows check it, and the corpus is full of real forms read back through it.
And the payment is not rounded. A number is a decimal128 — 34 significant
digits — and the arithmetic on it is exact to the end of those digits, so the
payment of £25,000 over sixty months at 5% a year is all thirty-four of them
and not "£471.78". Rounding it is the consumer's decision, not the form's.
Four ways a field can be missing
This is the part that matters most in practice.
| The field is | In the output document |
|---|---|
| answered | the value |
| answered, and deliberately empty | null |
| inapplicable | absent — the key is not there at all |
| never answered | null |
So:
- A field that does not apply — its
whenis false — is absent. The key is not in the document. Notnull: absent. - A field that applies but has no answer is
null. - A field that applies, has an answer, and the answer is empty is also
null.
The difference between absent and null is the difference between "this form
did not ask" and "this form asked and the answer was nothing". A consumer
that treats them the same will show a guarantor field that was never
applicable.
A worked example
form loan version = "1.0" {
field loan_amount { type = number }
field guarantor_name { type = string when = loan_amount > 10000 }
field notes { type = string }
field monthly { type = number value = pmt(0.05 / 12, 60, loan_amount) }
output {
loan_amount: number
guarantor_name: string
notes: string
monthly: number
}
}
Answer the form with loan_amount=5000:
{ "loan_amount": "5000", "notes": null, "monthly": "94.35616822005466422484060584474639" }
There is no guarantor_name key at all: the question was not asked, because
the amount is not over ten thousand.
Answer it with loan_amount=25000 instead, and the key appears — as null until
the guarantor is named:
{ "loan_amount": "25000", "guarantor_name": null, "notes": null, "monthly": "471.780841100273321124203029223732" }
Reading the output in the language
The output document is readable from inside the form, under $form.output:
field summary { type = string value = $form.output.monthly > 0 ? "has a payment" : "no payment" }
And $form.valid says whether the form is valid — which is what a submit
action guards on:
action submit {
method = "POST"
url = $env.api + "/applications"
when = $form.valid
body = $form.output
}
That is section 17 territory, but it is worth seeing here, because it is the end of the road: the document says what the form asks, the rules say what makes an answer valid, and the output block says what comes out.