Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

CODING CHALLENGE · N°62

Config Validator

Easy FDEReliabilityIntegrations

Half of "it doesn’t work at the customer" is a bad config — a missing key, a string where an int belongs, an env that isn’t allowed. Validate it up front and fail loudly with a clear list, instead of crashing three layers deep. Solve it in Python or TypeScript, with hidden tests.

The problem

Implement validate_config(config, spec). spec maps each key to a rule: {"type": "int"|"str"|"bool", "required": bool (optional), "allowed": [...] (optional)}. Return a sorted list of error strings: "missing: KEY" for a required key that’s absent, "wrong type: KEY" for a present key of the wrong type, and "not allowed: KEY" for a correctly-typed value outside its allowed list. A correct config returns [].

SOLVE IT YOURSELF

Your turn — write it

Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.

YOUR TASK

Implement validate_config(config, spec): for each spec key, handle absence (error only if required), then type, then allowed-values. Collect messages and return them sorted.

CPython · WebAssembly
Examples and constraints 4 examples
EXAMPLE 1
Input config = {"port":8080,"env":"x","debug":"yes"}, spec = {"port":{"type":"int","required":true},"env":{"type":"str","allowed":["dev","prod"]},"debug":{"type":"bool"},"host":{"type":"str","required":true}}
Output ["missing: host", "not allowed: env", "wrong type: debug"]
host absent, env not in allowed, debug is a string not a bool
EXAMPLE 2
Input config = {"port":80,"env":"dev"}, spec = {"port":{"type":"int"},"env":{"type":"str","allowed":["dev","prod"]}}
Output []
a valid config returns an empty list
EXAMPLE 3
Input config = {}, spec = {"b":{"type":"int","required":true},"a":{"type":"str","required":true}}
Output ["missing: a", "missing: b"]
errors come back sorted, not in spec order
EXAMPLE 4
Input config = {"n":true}, spec = {"n":{"type":"int"}}
Output ["wrong type: n"]
edge case: a bool is not an int here, even though Python’s bool subclasses int
CONSTRAINTS
  • A missing key is only an error if it is required; an absent optional key is fine and gets no further checks.
  • Type check first; only if the type is correct do you check allowed.
  • Booleans are not integers — True must fail an "int" check (and vice-versa).
  • Return the errors sorted.
Hints 4 ideas
HINTS — 4 IDEAS
  1. If the key isn’t in the config: push "missing: KEY" only when the rule is required, then skip it.
  2. Type-check with the exact type — and remember a bool is not an int in this exercise.
  3. If the type is wrong, record "wrong type: KEY" and don’t also check allowed.
  4. If typed correctly but there’s an allowed list the value isn’t in, record "not allowed: KEY".
Approach, complexity & discussion — open after you solve

Start from brute force

The obvious first draft walks the config: for each key, value in config.items(), look up spec[key], test the value with isinstance, and append any error. It is O(C) time for C config keys and O(E) space for E errors, so speed is not the issue; it is wrong in three ways. A required key that is absent never appears in the loop, so "missing: host" is never reported. A config key the spec does not mention raises KeyError on the lookup. And isinstance(True, int) is True in Python, because bool is a subclass of int, so {"n": true} slips through an int rule.

The approach

Drive the loop from the spec, because the spec is the only place that knows which keys must exist. Each key then goes through three checks in a fixed order, and the first failure stops it: presence, then type, then allowed values. That order is the invariant that gives each key at most one error: a missing key has no type to check, and a value of the wrong type cannot meaningfully be compared with an allowed list.

The reference solution loops over spec.items(). If key not in config, it appends "missing: " + key only when rule.get("required") is truthy, then continues. Otherwise it computes ok from the rule's type t; for "int" that is isinstance(value, int) and not isinstance(value, bool), which shuts booleans out. A failed ok appends "wrong type: " plus the key; a passing one checks value not in rule["allowed"] when the rule has that list. Every error is collected, and sorted(errors) makes the result independent of the spec's key order: the prefixes sort as missing, not allowed, wrong type, then by key.

Walk through an example

Trace the first example, where the spec's keys are visited in the order port, env, debug, host and the list is sorted only at the end.

stepkeyvaluecheck that decides iterrors
1port8080int, no allowed list: passes[]
2env"x"str passes; not in ["dev", "prod"]["not allowed: env"]
3debug"yes"bool expected, got a str["not allowed: env", "wrong type: debug"]
4hostabsentrequired, so missing["not allowed: env", "wrong type: debug", "missing: host"]
5——sorted(errors)["missing: host", "not allowed: env", "wrong type: debug"]

Complexity

Optimal: O(S + A + E log E) time for S spec keys, A total entries across the allowed lists (each is scanned linearly) and E errors to sort; O(E) space for the error list. Brute force (walking the config) is O(C) time and O(E) space for C config keys, but it cannot see absent required keys.

Edge cases to test

  • A present key whose value is null: {"host": null} against a required str rule → ["wrong type: host"], not "missing". The key is there; its value is the wrong type.
  • Booleans and integers in both directions: {"n": true} against an int rule → ["wrong type: n"], and {"flag": 1} against a bool rule → ["wrong type: flag"].
  • A whole-number float: in Python {"port": 8080.0} against an int rule → ["wrong type: port"], but the TypeScript reference returns [], because JavaScript has one number type and Number.isInteger(8080.0) is true.
  • A spec key that names an Object.prototype property: with {"toString": {"type": "str", "required": true}} and an empty config, Python reports "missing: toString", while TypeScript’s key in config walks the prototype chain, finds a function and reports "wrong type: toString". Object.hasOwn(config, key) avoids that.
  • Wrong type and outside the allowed list at once: {"env": 5} against {"type": "str", "allowed": ["dev"]} → only ["wrong type: env"]; the allowed check never runs.
  • An empty spec accepts any config, and config keys the spec does not mention are ignored: validate_config({"x": 1}, {}) → [].

Common mistakes

  • Failing on the first error instead of reporting all of them — that forces slow, one-at-a-time fixing.
  • Letting a bad value crash three layers deep instead of validating at the boundary.
  • Not distinguishing “missing key” from “wrong type”, so the message is not actionable.
  • Checking isinstance(value, int) alone, which accepts True and False as integers.

Follow-ups an interviewer asks next

How would you validate nested configs?
Add an "object" type whose rule carries its own spec, and a "list" type with a rule for its items, then recurse. Pass the path down so messages stay actionable: "wrong type: db.port" or "missing: servers[2].host". The cost stays linear in the number of values checked, and sorting by path keeps the output stable.
How would you catch typos such as "prot" instead of "port"?
Right now an unknown key is silently ignored, so the typo passes; the real key is then reported missing if it is required, or quietly left at its default if it is not. After the spec loop, walk the config’s own keys and emit "unknown: KEY" for any the spec lacks. Python’s difflib.get_close_matches can suggest the nearest spec key in the message.
What changes when the config comes from environment variables?
Everything arrives as a string, so "8080" fails an int rule. Parse before you type-check: int(value) for integers, and an explicit set such as {"true", "1", "yes"} for booleans. Never use bool(value), because bool("false") is True for any non-empty string.
Would you write this by hand in production?
Usually not. JSON Schema has validators in most languages, and Pydantic in Python or Zod in TypeScript cover nesting, defaults, coercion and custom messages, and they report every error at once rather than the first. The design rules are the same ones this exercise teaches: validate at the boundary, check presence before type before value, and collect everything.

Where this shows up

Half of “it doesn’t work at the customer” is a bad config — a missing key, a string where an int belongs, an env that isn’t allowed. Validating up front and failing with a clear list beats a cryptic crash deep in the stack, which is why this is a core forward-deployed and integrations skill: turn config errors into actionable messages at the edge.

Reference solution

Reference solution — Python
def validate_config(config, spec):
    errors = []
    for key, rule in spec.items():
        if key not in config:
            if rule.get("required"):
                errors.append("missing: " + key)
            continue
        value = config[key]
        t = rule["type"]
        ok = (
            (t == "int" and isinstance(value, int) and not isinstance(value, bool))
            or (t == "str" and isinstance(value, str))
            or (t == "bool" and isinstance(value, bool))
        )
        if not ok:
            errors.append("wrong type: " + key)
        elif "allowed" in rule and value not in rule["allowed"]:
            errors.append("not allowed: " + key)
    return sorted(errors)


print(validate_config(
    {"port": 8080, "env": "x", "debug": "yes"},
    {"port": {"type": "int", "required": True},
     "env": {"type": "str", "allowed": ["dev", "prod"]},
     "debug": {"type": "bool"},
     "host": {"type": "str", "required": True}}))
# -> ['missing: host', 'not allowed: env', 'wrong type: debug']
Reference solution — TypeScript
function validateConfig(config: any, spec: any): string[] {
  const errors: string[] = [];
  for (const [key, rule] of Object.entries<any>(spec)) {
    if (!(key in config)) {
      if (rule.required) errors.push("missing: " + key);
      continue;
    }
    const value = config[key];
    const t = rule.type;
    const ok =
      (t === "int" && Number.isInteger(value) && typeof value === "number") ||
      (t === "str" && typeof value === "string") ||
      (t === "bool" && typeof value === "boolean");
    if (!ok) errors.push("wrong type: " + key);
    else if (rule.allowed && !rule.allowed.includes(value)) errors.push("not allowed: " + key);
  }
  return errors.sort();
}

console.log(JSON.stringify(validateConfig(
  { port: 8080, env: "x", debug: "yes" },
  { port: { type: "int", required: true },
    env: { type: "str", allowed: ["dev", "prod"] },
    debug: { type: "bool" },
    host: { type: "str", required: true } })));
// -> ["missing: host","not allowed: env","wrong type: debug"]

Questions about Config Validator

Why does True fail an int check in this config validator?

In Python, bool is a subclass of int, so isinstance(True, int) returns True and True == 1. A validator that only asks isinstance(value, int) therefore accepts a boolean where a port number or a retry count belongs, which is almost always a bug in the config. The reference solution adds and not isinstance(value, bool) to the int check, and the bool check only accepts real booleans, so 1 fails a bool rule too. In TypeScript the problem does not arise, because typeof true is boolean and typeof 1 is number.

Should a config validator stop at the first error?

No. Stopping at the first error means the operator fixes one problem, redeploys, and meets the next one, which turns a five-minute fix into an afternoon. Collecting every error in one pass gives them the complete list at once. The only errors worth suppressing are the ones caused by an earlier failure on the same key: if a value has the wrong type, checking it against the allowed list adds nothing, so the validator records the type error and moves on to the next key instead of reporting two messages for one mistake.

What is the difference between a missing key and a wrong type error?

A missing key is absent from the config entirely, and it is only an error when the rule marks it as required; an absent optional key produces nothing. A wrong type error means the key is present but its value has the wrong type. That includes a value of null or None, which is a common surprise: the key exists, so the validator reports wrong type rather than missing. Keeping the two messages separate tells the operator whether to add a line to the config or fix the value already there.

Why are the validation errors returned sorted?

Sorting makes the output deterministic. Without it, the order of errors depends on the order of keys in the spec, which is an accident of how the spec was written and can change between versions, so tests and diffs become flaky. Because every message starts with its kind, sorting also groups them usefully: all the missing keys first, then the not allowed values, then the wrong types, each group in alphabetical order by key. A reader can scan the list and see at a glance which kind of mistake dominates.

Finished this one? 0 / 76 Challenges done
NextExponential Backoff with Full Jitter Next on the “Integrations & messy data” ladder · rung 4 of 10

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More Challenges