Practical infra coding

Flatten a nested JSON config into dotted keys

easyCSV, JSON and YAML

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 null print 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

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.

  1. If the value is a non-empty dict, recurse into each item with prefix.key (just key at the top level).
  2. If it is a non-empty list, recurse with prefix[i].
  3. Otherwise yield (prefix, value). This case covers scalars, None and empty containers, so nothing is dropped.
  4. render prints strings bare and everything else with json.dumps. True becomes true and None becomes null, 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.

ComplexityTime O(n)Space O(d)
Python
import json
import 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, secret or token.

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.