DEV Community

Shubham Anandani
Shubham Anandani

Posted on Originally published at imshubh.com on

Null Isn’t False: Building Fair Credential Verifiers

One of the subtle but damaging patterns in identity systems is treating null as false. If a credential lacks a field, or a proof is absent, some code paths flip a boolean to false and move on. In verification, this collapses “we don’t know” into “disproven.” The difference matters: missing evidence isn’t proof of falsehood.

The failure mode

A verifier expects a proofOfX field. The credential arrives without it. The code sets proofOfX=false. A downstream gate denies access. Later, it turns out proofOfX wasn’t required for this flow, or the holder didn’t have a way to request it yet. The deny was based on an absence, not a disproven claim.

What I learned

Model states, not booleans

Replace booleans with intent-rich types. For a given proof, consider states like these:

  • Present: The evidence arrived and checks out.
  • Missing: The evidence was not provided.
  • Expired: The evidence was valid but is past its lifetime.
  • Revoked: The issuer withdrew the credential.
  • Unverifiable: The evidence can’t be checked (e.g., failed binding).
  • Invalid: The evidence is malformed or has a bad signature.

This simple taxonomy avoids flattening all “not present” into “false.”

Exhaustive decision logic at boundaries

At the verifier boundary, use exhaustive switches over the state. Force downstream callers to handle each branch. If your language supports pattern matching, lean on it to eliminate default fall-through behaviors.

Treat absence as an opportunity to request or fall back

If a required field is missing, decide whether to:

  1. Prompt the holder to supply it (if applicable).
  2. Proceed with reduced assurance and log the state.
  3. Deny the request explicitly as “unverifiable,” not “invalid.”

The right action depends on your policy and context; the key is keeping “unverifiable” distinct.

Make “invalid” a narrow, technical category

Invalid should mean “structured wrong,” “bad signature,” or “contradiction.” It shouldn’t include “not provided,” “not bound,” or “not present yet.” Narrow “invalid” ensures you don’t over-penalize users for the system’s constraints.

Propagate state through APIs and UX

Expose the state in API responses: include status: "unverifiable" with a short note. Avoid returning false or 0 as a sentinel for “missing.” In UX, explain that more evidence is needed or that a different path is available.

Beware implicit type coercions

JSON, databases, and ORMs often coerce nulls to zero or false. Audit these boundaries. Ensure your serializers and persistence layers preserve state labels rather than defaulting values.

Test for fairness: counterfactuals

Put fairness into tests. For example: if proofOfX is missing, does the system deny or ask for more evidence? Are there alternative flows? Keep tests that assert the difference between “unverifiable” and “invalid.”

Explicit verification outcomes

from dataclasses import dataclass
from typing import Literal

Status = Literal["accepted", "unverifiable", "invalid"]

@dataclass(frozen=True)
class FieldCheck:
    name: str
    status: Status
    note: str | None = None
Enter fullscreen mode Exit fullscreen mode

Trade-offs and failure modes

Trade-offs

  • More state complexity vs simplicity: Explicit states add a little code. The clarity and fairness gains are worth it.
  • UX friction vs silent denial: Asking for more evidence can add steps. Silent denials are worse. Balance friction with guidance.
  • Logging granularity vs privacy: Notes improve debugging; ensure they contain no sensitive data.

Failure modes

  • “Hidden coercion”: Tooling collapses null to false. Solution: audit boundaries and enforce state labels.
  • “Catch-all invalid”: Development shortcuts treat everything negative as invalid. Solution: write exhaustive switches and linters.
  • “Unverifiable loop”: Repeated prompts for evidence without alternative paths. Solution: define fallback policies clearly.

Try it yourself

  1. Replace boolean checks for critical credential fields with stateful outcomes (“accepted,” “unverifiable,” “invalid”).
  2. Expose the state in your verifier’s API. Track how often “unverifiable” occurs and address UX with prompts or fallbacks.
  3. Add tests that assert denials only occur for “invalid,” not “unverifiable.”

Further reading

Top comments (0)