skip to content

In Rego, what does `not input.metadata.annotations.exempt` evaluate to if the object has no annotations?

level: middleimportance: should knowfreq 46%

answer

  1. negation succeeds on undefined or false
  2. a missing key passes the negation
  3. absent and false look the same
  4. typos inside not fail the other way
  5. not tests, it never binds

basics

~20 s

It succeeds. A negated expression in Rego is satisfied whenever the inner expression is undefined or false, so a negated reference to a path that does not exist always passes — absent and explicitly-not-set look identical to the rule.

solid answer

~50 s

`not E` succeeds when `E` is undefined or `false`, and only then. With no `annotations` key at all, `input.metadata.annotations.exempt` is undefined, so the negation is satisfied and the body carries on as though the object were not exempt. That is usually the behaviour you want for an exemption guard — objects that never asked for an exemption should not get one. The trap is that negation flips which direction a mistake fails in. A typo in a *positive* expression makes the body undefined and the rule quietly stops firing; the same typo inside `not` makes the guard vacuously true, so the rule fires for everything, including the objects the guard was supposed to skip. `not` also cannot tell "the key is absent" from "the key is present and false", and it cannot bind variables — a negated expression containing an unbound variable is unsafe and the policy will not compile.

go deeper

for a junior

Remember the one-line rule: a negated Rego expression is satisfied when the inner expression is undefined or false. A missing annotation therefore passes a not check rather than failing it.

for a middle

Explain why that is the right default for an exemption guard, and show the flip side — inside a negation, a wrong path becomes vacuously true, so the rule over-fires instead of under-firing. Know that not cannot bind variables.

for a senior

Demonstrate judgment about when negation is the wrong tool: as soon as the decision depends on an exemption's value, owner or expiry, bind and compare it with an explicit default rather than testing its absence.

for a principal

Be ready to argue for a house style on exemption markers — validated values over bare presence — because a guard that only tests absence cannot support expiry, ownership or review of the exemptions it grants.

## The rule In Rego, `not E` is satisfied exactly when `E` is undefined or `false`. If `E` is defined and not `false`, the negation is not satisfied and the surrounding body is undefined. That single sentence covers the whole behaviour, and it has one consequence people find surprising: **negation over data that is not there always succeeds**. `not input.metadata.annotations.exempt` passes for an object with no `annotations` map, for an object whose annotations map lacks the key, and for an object where the key is present with the value `false`. Three quite different situations, one indistinguishable result. ## Why this is usually right For the common case — an exemption guard on a deny rule — this is exactly the semantics you want: ``` deny contains msg if { not input.metadata.annotations["policy.example.com/exempt"] ... } ``` An object that never requested an exemption has no such annotation, the negation succeeds, and the rule proceeds to check it. If negation over absent data failed instead, you would have to annotate every object in the estate just to state that it is not exempt. ## Why it bites The interesting property of `not` is that it **reverses the direction in which a mistake fails**. - A mistyped path in a *positive* expression makes the body undefined. The rule stops matching, nothing is denied, and the failure is silent — you find out when someone asks why the gate has never fired. - The same mistyped path inside `not` is *always* satisfied. The guard becomes vacuously true, the rule fires for objects it was meant to skip, and the failure is loud — a wave of denials on workloads that hold a legitimate exemption. Neither is good, but they need different reflexes. The silent one requires you to prove the rule can still deny; the loud one announces itself the moment it ships and mostly costs you trust with the team that got blocked. The second bite is that `not` erases the difference between absent and false. If your policy needs to distinguish "this object explicitly declared itself non-exempt" from "this object says nothing about exemptions" — which matters the moment an exemption carries an expiry, an owner or a ticket reference — negation cannot do it for you. ## Distinguishing absent from false Bind the value and compare it, rather than testing it through a negation: ``` annotations := object.get(input.metadata, "annotations", {}) exempt := object.get(annotations, "policy.example.com/exempt", "absent") ``` Now `exempt == "absent"`, `exempt == "false"` and `exempt == "true"` are three testable states, and the rule can require a real, well-formed exemption value rather than merely the absence of a contradiction. `object.get` returns its default when the key is missing, which is what turns undefined into a value you can reason about. ## Two more properties worth knowing **`not` never binds.** A negated expression containing a variable that is not already bound elsewhere in the body is unsafe, and the compiler rejects the policy — this is a load-time error, not a silent one. Negation tests; it does not produce bindings for later expressions to use. If you want "there is no container that violates X", express it as a negation over a rule or a comprehension that is itself fully defined, so the variable is bound inside that construct rather than under the `not`. **Double negation is not identity.** `not not E` is satisfied when `E` is defined and not false — it collapses the distinction between undefined and false in the opposite direction from what people usually intend, and reading it in a review is much harder than reading a positive test. Prefer binding a value and comparing it. ## What to say in an interview State the rule first — negation succeeds on undefined or false — then show that you know which way it makes mistakes fail, and finish with the practical guidance: use `not` for "this marker is absent", use an explicit bound comparison whenever the difference between absent and false is meaningful to the decision, and never rely on a negation to prove a field was checked.

  • So does a typo fail open or closed — and does the polarity of the expression change the answer?
    The polarity decides it. A typo in a positive expression makes the body undefined, so the rule silently stops firing — fail-open and invisible. The same typo inside `not` makes the negation vacuously true, so the rule fires for everything including objects that hold a valid exemption — noisy, but you find out immediately. Silent under-enforcement is the more dangerous half.
  • How would you distinguish an annotation that is absent from one explicitly set to false?
    Bind the value with a sentinel default instead of negating it: `v := object.get(input.metadata, "annotations", {})` then look the key up with a default like `"absent"`. Comparing that bound value gives you three distinguishable states, which is what you need once an exemption has to carry a real, validated value rather than merely exist.
  • Can a variable first introduced inside a `not` be used later in the body?
    No. A negated expression containing an unbound variable is unsafe and OPA rejects the policy when it is compiled, so this is a load-time error rather than a silent undefined. Negation only tests; bindings must come from a positive expression, a comprehension or a helper rule earlier in the body.

Asking not exempt is like checking a box marked "no waiver on file". An empty file drawer and a waiver stamped VOID both tick the box; only opening the folder tells them apart.

saying these in an interview costs you the question

  • Says a negated missing field evaluates to false
  • Expects an error when negating an absent path
  • Uses not to prove a field was actually checked
  • Assumes not distinguishes absent from explicitly false
  • Introduces a new variable inside a negated expression

context