Flatten a nested JSON config into dotted keys
Problem statement
A deploy tool expects configuration as flat key=value lines, but the service config is nested JSON. Flatten it:
- Nested object keys are joined with
.:database.host. - List items use their index in brackets:
upstreams[0].name,tags[1]. - Strings print as-is. Numbers, booleans and
nullprint as JSON:true,false,null. - An empty object or list is a value in its own right and prints as
{}or[]. It must not disappear. - Keep the key order of the file.
If the file is not valid JSON, print file:line:column: invalid JSON: reason and exit 1.
config.json
{ "service": "checkout", "replicas": 3, "database": {"host": "db.internal", "port": 5432, "tls": true}, "features": {"new_ui": false, "beta": {}}, "upstreams": [ {"name": "payments", "url": "http://payments:8080"}, {"name": "inventory", "url": "http://inventory:8080"} ], "tags": ["prod", "eu-west-1"], "owner": null}Examples
Example 1
Input: python solution.py config.json
Output: service=checkout
replicas=3
database.host=db.internal
database.port=5432
database.tls=true
features.new_ui=false
features.beta={}
upstreams[0].name=payments
upstreams[0].url=http://payments:8080
upstreams[1].name=inventory
upstreams[1].url=http://inventory:8080
tags[0]=prod
tags[1]=eu-west-1
owner=null
Explanation: features.beta is an empty object and is kept. Python's True is printed as true because non-strings go through json.dumps.
Hints
Approach
Optimal
Recursive descent over the JSON tree.
- If the value is a non-empty dict, recurse into each item with
prefix.key(justkeyat the top level). - If it is a non-empty list, recurse with
prefix[i]. - Otherwise yield
(prefix, value). This case covers scalars,Noneand empty containers, so nothing is dropped. renderprints strings bare and everything else withjson.dumps.TruebecomestrueandNonebecomesnull, which the consuming tool understands.
Python dicts preserve insertion order and json.load builds them in file order, so the output follows the file. Time is linear in the number of nodes n. Space is the recursion depth d plus the generator frames.
O(n)Space O(d)import jsonimport sys def flatten(value, prefix=""): """Yield (dotted_key, leaf) pairs. Lists use [i]; empty containers are kept.""" if isinstance(value, dict) and value: for k, v in value.items(): yield from flatten(v, f"{prefix}.{k}" if prefix else str(k)) elif isinstance(value, list) and value: for i, v in enumerate(value): yield from flatten(v, f"{prefix}[{i}]") else: yield prefix, value # scalar, null, {} or [] def render(leaf): # strings print bare; everything else as JSON so true/null/{} stay unambiguous return leaf if isinstance(leaf, str) else json.dumps(leaf) def main(path): try: with open(path, encoding="utf-8") as f: config = json.load(f) except json.JSONDecodeError as e: sys.exit(f"{path}:{e.lineno}:{e.colno}: invalid JSON: {e.msg}") for key, leaf in flatten(config): print(f"{key}={render(leaf)}") if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else "config.json")Follow-up questions
- Write the reverse: turn the flat lines back into nested JSON.
- Output environment variable names instead:
DATABASE_HOST=db.internal. - Mask values whose key contains
password,secretortoken.
Frequently asked questions
Then a.b is ambiguous: two levels, or one key with a dot? Tools solve this in different ways: escaping (app\.kubernetes\.io), bracket syntax (labels["app.kubernetes.io/name"]), or a different separator. Mention it; Kubernetes labels are the usual real-world case.
Flattening is how nested config turns into environment variables, Consul or SSM parameter paths, and Helm --set flags. It is also the building block for diffing two configs. The interviewer is checking recursion, type handling and edge cases such as empty containers and null.
Only with nesting several hundred levels deep. Python's default recursion limit is 1,000, and real configs are rarely deeper than 10. If you need to handle hostile input, rewrite it with an explicit stack of (prefix, value) pairs.