aontu

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.