Katkat

Design a JSON Merge API with Pluggable Behaviors

TeslaSoftware Engineer · Technical Screen

Last updated

Implement a non-mutating JSON merge utility with recursive object handling, configurable array merge strategies, and an extensible design for custom merge policies.

Problem Statement

Configuration management systems often need to merge JSON documents (such as base defaults and environment-specific overrides). In this task, you will design and implement a non-mutating JSON merge function that supports pluggable strategies for handling arrays.

Your function should accept a base document, a patch document, and an optional configuration controlling array merge behavior. Neither input should be modified in place.

Merge Rules

  1. Objects (dicts): If both values are JSON objects, merge them recursively. Keys existing only in base are retained. Keys existing only in patch are added. Keys present in both are merged recursively.
  2. Arrays (lists): If both values are JSON arrays, behavior is determined by array_strategy:
    • "overwrite" (default): The patch array replaces the base array.
    • "append": Elements from patch are appended to the end of base.
  3. Type Mismatches & Primitives: If the types of base and patch do not match (e.g., a dict vs. an int) or if both values are primitive types (strings, numbers, booleans, null/None), the patch value replaces the base value.
  4. Immutability: Neither base nor patch should be modified. The returned structure should be independent of changes to the inputs.

Interface

from typing import Any, Literal

def merge_json(
    base: Any,
    patch: Any,
    array_strategy: Literal["overwrite", "append"] = "overwrite"
) -> Any:
    pass

Examples

Example 1

base = {
    "device": "telemetry-v1",
    "enabled": True,
    "thresholds": {"speed": 120, "temp": 75},
    "tags": ["active"]
}
patch = {
    "enabled": False,
    "thresholds": {"temp": 80, "pressure": 32},
    "tags": ["verified"]
}

# With array_strategy="overwrite" (default)
result = merge_json(base, patch)
# Output:
# {
#     "device": "telemetry-v1",
#     "enabled": False,
#     "thresholds": {"speed": 120, "temp": 80, "pressure": 32},
#     "tags": ["verified"]
# }

Example 2

# Using the same base and patch with array_strategy="append"
result = merge_json(base, patch, array_strategy="append")
# Output:
# {
#     "device": "telemetry-v1",
#     "enabled": False,
#     "thresholds": {"speed": 120, "temp": 80, "pressure": 32},
#     "tags": ["active", "verified"]
# }

Constraints & Edge Cases

  • Input structures consist of valid JSON types: dict, list, str, int, float, bool, and None.
  • Maximum nesting depth is <= 100 levels.
  • Assume no circular references exist within inputs.
  • Ensure None in patch is treated as a valid value that overwrites base (e.g., merge_json({"a": 1}, {"a": None}) results in {"a": None}).

Hints

Hint 1

Handle the base cases first: if types differ or either value is not a dict/list, return a deep copy of the patch.

Hint 2

For dictionaries, construct a new dict by iterating through the union of all keys from both base and patch, delegating to recursive calls.

Hint 3

Structure your array merging logic using a dictionary of strategy functions so that new strategies (e.g., union, deduplicate) can be added without altering the core recursion logic.

Follow-up questions

  • How would you extend this API to allow path-specific merge strategies (e.g., append for `service.tags` but overwrite for `service.ports`)?
  • How can you avoid full deep copies on large unmodified subtrees while still guaranteeing immutability (structural sharing)?
  • How would you support JSON Merge Patch (RFC 7396) semantics where a key mapped to `null` deletes the key from the target object?