Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Config Validator
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 [].
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.
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.
Examples and constraints 4 examples
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}}["missing: host", "not allowed: env", "wrong type: debug"]config = {"port":80,"env":"dev"}, spec = {"port":{"type":"int"},"env":{"type":"str","allowed":["dev","prod"]}}[]config = {}, spec = {"b":{"type":"int","required":true},"a":{"type":"str","required":true}}["missing: a", "missing: b"]config = {"n":true}, spec = {"n":{"type":"int"}}["wrong type: n"]- 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 —
Truemust fail an"int"check (and vice-versa). - Return the errors sorted.
Hints 4 ideas
- If the key isn’t in the config: push
"missing: KEY"only when the rule is required, then skip it. - Type-check with the exact type — and remember a bool is not an int in this exercise.
- If the type is wrong, record
"wrong type: KEY"and don’t also check allowed. - If typed correctly but there’s an
allowedlist 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.
| step | key | value | check that decides it | errors |
|---|---|---|---|---|
| 1 | port | 8080 | int, no allowed list: passes | [] |
| 2 | env | "x" | str passes; not in ["dev", "prod"] | ["not allowed: env"] |
| 3 | debug | "yes" | bool expected, got a str | ["not allowed: env", "wrong type: debug"] |
| 4 | host | absent | required, 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 andNumber.isInteger(8080.0)istrue. - A spec key that names an
Object.prototypeproperty: with{"toString": {"type": "str", "required": true}}and an empty config, Python reports"missing: toString", while TypeScript’skey in configwalks 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 acceptsTrueandFalseas 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’sdifflib.get_close_matchescan 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 usebool(value), becausebool("false")isTruefor 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.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Retry with Exponential BackoffTools time out and APIs rate-limit — a production agent retries without hammering. Exponential backoff waits longer after each failure (1s, 2s, 4s, 8s…) up to a cap. Compute the delay schedule: delay[i] = min(cap, base·2ⁱ). Solve it in Python or TypeScript.Read →
- Verify a Webhook SignatureEvery real integration sends signed webhooks — and every FDE has to verify them. Recompute the signature over the payload, compare in constant time, and reject stale events to stop replay attacks. Solve it in Python or TypeScript, with hidden tests.Read →
- Idempotency Key HandlerNetworks retry. A webhook fires twice; a payment is re-sent after a timeout. Without idempotency you double-charge or double-write. The fix: dedupe by a client-supplied key and always return the first result. Solve it in Python or TypeScript, with hidden tests.Read →
- CSV → Schema MapperEvery customer hands you a messy export. Before it flows into your system, each row must be coerced to a schema — strings to ints, "true" to booleans — and the rows that don’t fit quarantined, not silently dropped. Solve it in Python or TypeScript, with hidden tests.Read →
- Data Reconciliation DiffThe customer swears the migration worked. You reconcile: compare source records against what landed in the target, keyed by id, and report exactly what was added, removed, or changed. Solve it in Python or TypeScript, with hidden tests.Read →
- PII RedactorBefore customer data touches a log line — or a third-party LLM — the personal bits have to go. Mask emails, SSNs, credit cards, and phone numbers with pattern matching, leaving the rest readable. Solve it in Python or TypeScript, with hidden tests.Read →