Forbid unexpected keys
Seal a map with `close` so a typo'd or invented key is refused instead of absorbed.
Rendered from
docs/how-to/forbid-unexpected-keys.md
in the engine repository, where a correction belongs, and where the
test suite executes every example on this page.
Maps are open by default: a key the schema never mentioned unifies in
without comment. That openness is what makes composition work, and it
is exactly wrong for config. Seal the map with close:
config: close({ host:string port:integer })
config: { host:h port:1 }
{
"config": {
"host": "h",
"port": 1
}
}
Declared keys compose as before. Now add a key the schema does not
declare. Write this as config.aon:
config: close({ host:string port:integer })
config: { host:h port:1 debug:true }
$ aontu config.aon
[aontu/closed]: Cannot resolve value at path $.config.debug
...
$ echo $?
1
The error names the key, which is the point: an absorbed debug
would have run in production doing nothing.
open(x) lifts a seal again, so a schema you import closed can be
extended deliberately:
a: open(close({ x:1 })) & { y:2 }
{
"a": {
"x": 1,
"y": 2
}
}
What to watch for: close seals exactly the node it wraps, and only
that node. Children generated inside it stay open unless you seal
them too: around a pack generator that difference decides whether
a misspelled override is refused or absorbed, and seal generated
children deeply walks through it. A
list tail is also not closed by the enclosing map’s seal; the
element template is: see
constrain every element of a list.
The semantics are specified in Closed values: close /
open.
Closedness is also the one thing aontu and JSON Schema say
identically (additionalProperties: false): see
export JSON Schema.