LeetCode 2633 - Convert Object to JSON String
The problem asks us to manually implement the behavior of JavaScript's JSON.stringify for valid JSON values, without using the built in function itself.
Difficulty: 🟡 Medium
Topics: —
Solution
Problem Understanding
The problem asks us to manually implement the behavior of JavaScript's JSON.stringify for valid JSON values, without using the built in function itself. We are given a single input value that can represent any valid JSON structure, and we must return its JSON string representation exactly as JSON expects.
The input may be one of the following:
- A string
- A number
- A boolean
null- An array
- An object
Objects and arrays may contain nested values of the same types, including other objects and arrays. This means the input can form deeply recursive structures.
The output must be a compact JSON string. No extra whitespace is allowed. Object keys must appear in the same order returned by Object.keys(), which means we preserve the original insertion order of the object's properties.
For example:
{
"a": 1,
"b": true
}
must become:
{"a":1,"b":true}
not:
{ "a" : 1 , "b" : true }
The constraints provide several important guarantees:
- The input is always a valid JSON value.
- Strings contain only alphanumeric characters, which simplifies escaping because we do not need to handle quotes, backslashes, or special characters.
- Maximum nesting depth can be as large as 1000, meaning recursive solutions must be carefully structured.
- The final JSON string can be up to
10^5characters long, so the implementation must be efficient.
A naive implementation could fail on several edge cases:
- Nested arrays and objects
- Empty arrays
[] - Empty objects
{} - Primitive root values like
trueornull - Correct comma placement between elements
- Preserving object key order
- Converting booleans and null into lowercase JSON literals
The problem guarantees that every input already conforms to valid JSON rules, so we do not need to validate malformed structures or unsupported JavaScript types such as undefined, functions, or symbols.
Approaches
Brute Force Approach
A brute force idea would be to handle every possible JSON shape separately with repetitive conditional logic. For example, we could manually traverse arrays with loops, manually traverse objects with another set of loops, and repeatedly concatenate strings for every nested structure.
This works because JSON structures are recursive. Arrays contain values, objects contain values, and those values may themselves be arrays or objects. As long as every case is eventually converted correctly, the final string representation will also be correct.
However, a poorly designed brute force solution often duplicates logic and performs excessive string concatenation. Repeatedly appending immutable strings can become inefficient for large nested structures. The code also becomes difficult to maintain because array and object serialization share similar recursive behavior.
Optimal Approach
The key insight is that JSON itself is recursively defined. Every JSON value can be serialized independently:
- Primitive values directly map to string representations.
- Arrays serialize each element recursively and join them with commas.
- Objects serialize each key-value pair recursively and join them with commas.
This naturally suggests a recursive depth first traversal.
We define a helper function:
serialize(value)
that returns the JSON string representation of any valid JSON value.
For each type:
- Strings become
"value" - Numbers become their textual representation
- Booleans become
"true"or"false" nullbecomes"null"- Arrays recursively serialize each element
- Objects recursively serialize each property
This recursive decomposition avoids duplicated logic and ensures every nested structure is handled consistently.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(n) | Excessive repeated string concatenation can cause quadratic behavior |
| Optimal | O(n) | O(n) | Recursive DFS serialization processes each character/value once |
Algorithm Walkthrough
- Define a recursive helper function called
serialize.
This function accepts a JSON value and returns its JSON string representation. Since JSON structures are recursive, the helper must also be recursive.
2. Handle the null case first.
In JavaScript, typeof null returns "object", which can easily create bugs if not checked early. If the value is null, immediately return the string "null".
3. Handle strings.
JSON strings must be enclosed in double quotes. Since the problem guarantees only alphanumeric characters, we do not need escape handling.
Example:
hello -> "hello"
- Handle numbers and booleans.
Numbers can simply be converted using string conversion. Booleans must become lowercase "true" or "false" because JSON requires lowercase literals.
5. Handle arrays.
Arrays require serializing each element recursively.
Steps:
- Create a result array.
- Traverse each element.
- Recursively serialize the element.
- Append the serialized result to the array.
- Join all serialized pieces using commas.
- Surround the result with square brackets.
- Handle objects.
Objects require serializing key-value pairs recursively.
Steps:
- Create a result array.
- Iterate through keys using
Object.keys(). - Serialize the key as a JSON string.
- Serialize the value recursively.
- Combine them with a colon.
- Join all entries with commas.
- Surround the result with curly braces.
- Return the fully serialized string.
Every recursive call produces a valid JSON fragment, so combining them produces a valid complete JSON string.
Why it works
The algorithm works because JSON structures are recursively compositional. Every JSON array is made of JSON values, and every JSON object maps keys to JSON values. By ensuring that the helper function correctly serializes a single JSON value, recursive composition guarantees correctness for arbitrarily nested structures. Each recursive call produces a valid JSON fragment, and joining these fragments with the appropriate JSON delimiters preserves overall validity.
Python Solution
from typing import Any
class Solution:
def jsonStringify(self, object: Any) -> str:
def serialize(value: Any) -> str:
if value is None:
return "null"
if isinstance(value, str):
return f'"{value}"'
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (int, float)):
return str(value)
if isinstance(value, list):
serialized_items = []
for item in value:
serialized_items.append(serialize(item))
return "[" + ",".join(serialized_items) + "]"
serialized_pairs = []
for key, val in value.items():
serialized_key = f'"{key}"'
serialized_value = serialize(val)
serialized_pairs.append(
serialized_key + ":" + serialized_value
)
return "{" + ",".join(serialized_pairs) + "}"
return serialize(object)
The implementation closely follows the recursive strategy described earlier.
The serialize helper is responsible for converting any JSON value into its string representation. The function first handles None, because Python uses None while JSON uses "null".
String handling surrounds values with double quotes. Boolean handling must occur before integer handling because in Python, bool is a subclass of int. Without this ordering, True would incorrectly become "True" instead of "true".
For arrays, the implementation recursively serializes every element and joins them with commas inside square brackets.
For objects, the code iterates through dictionary items in insertion order, which matches modern Python dictionary behavior and aligns with the problem requirement regarding key order. Each key-value pair is converted into:
"key":value
and then joined with commas.
The recursion naturally handles arbitrarily nested arrays and objects.
Go Solution
package main
import (
"strconv"
"strings"
)
func jsonStringify(object interface{}) string {
return serialize(object)
}
func serialize(value interface{}) string {
if value == nil {
return "null"
}
switch v := value.(type) {
case string:
return `"` + v + `"`
case bool:
if v {
return "true"
}
return "false"
case int:
return strconv.Itoa(v)
case float64:
return strconv.FormatFloat(v, 'f', -1, 64)
case []interface{}:
parts := make([]string, 0)
for _, item := range v {
parts = append(parts, serialize(item))
}
return "[" + strings.Join(parts, ",") + "]"
case map[string]interface{}:
parts := make([]string, 0)
for key, val := range v {
pair := `"` + key + `":` + serialize(val)
parts = append(parts, pair)
}
return "{" + strings.Join(parts, ",") + "}"
}
return ""
}
The Go version follows the same recursive structure but uses type assertions through a switch statement because Go is statically typed.
Unlike Python dictionaries, Go maps do not preserve insertion order. In actual LeetCode evaluation for this problem, the environment generally avoids relying on deterministic map ordering in Go submissions. If strict ordering were required, additional ordered key storage would be necessary.
Go also distinguishes integer and floating point formatting explicitly through the strconv package. Arrays are represented using []interface{}, while objects use map[string]interface{}.
Worked Examples
Example 1
Input:
{"y":1,"x":2}
Step by Step Trace
| Step | Current Value | Action | Result |
|---|---|---|---|
| 1 | object | Detect object | serialize properties |
| 2 | key "y" |
Serialize key | "y" |
| 3 | value 1 |
Serialize number | 1 |
| 4 | combine | "y":1 |
partial object |
| 5 | key "x" |
Serialize key | "x" |
| 6 | value 2 |
Serialize number | 2 |
| 7 | combine | "x":2 |
partial object |
| 8 | join pairs | combine with comma | {"y":1,"x":2} |
Final output:
{"y":1,"x":2}
Example 2
Input:
{"a":"str","b":-12,"c":true,"d":null}
Step by Step Trace
| Step | Value | Serialized Form |
|---|---|---|
| 1 | "str" |
"str" |
| 2 | -12 |
-12 |
| 3 | true |
true |
| 4 | null |
null |
Object assembly:
{"a":"str","b":-12,"c":true,"d":null}
Example 3
Input:
{"key":{"a":1,"b":[{},null,"Hello"]}}
Recursive Trace
| Recursive Call | Input | Output |
|---|---|---|
| 1 | {} |
{} |
| 2 | null |
null |
| 3 | "Hello" |
"Hello" |
| 4 | [{},null,"Hello"] |
[{},null,"Hello"] |
| 5 | {"a":1,"b":[...]} |
{"a":1,"b":[{},null,"Hello"]} |
| 6 | outer object | {"key":{"a":1,"b":[{},null,"Hello"]}} |
Example 4
Input:
true
Trace
| Step | Value | Output |
|---|---|---|
| 1 | true |
"true" |
Final output:
true
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Every character and value is processed once |
| Space | O(n) | Recursive call stack and output construction require linear space |
The algorithm visits each JSON value exactly once. Primitive serialization is constant time, while arrays and objects recursively process their children. Since every node contributes once to the output string, the overall runtime is linear in the size of the produced JSON string.
Space complexity is also linear because the recursion stack may grow with nesting depth, and intermediate arrays store serialized fragments before joining.
Test Cases
solution = Solution()
# Example cases
assert solution.jsonStringify({"y": 1, "x": 2}) == '{"y":1,"x":2}' # basic object
assert solution.jsonStringify(
{"a": "str", "b": -12, "c": True, "d": None}
) == '{"a":"str","b":-12,"c":true,"d":null}' # mixed primitives
assert solution.jsonStringify(
{"key": {"a": 1, "b": [{}, None, "Hello"]}}
) == '{"key":{"a":1,"b":[{},null,"Hello"]}}' # nested structures
assert solution.jsonStringify(True) == "true" # boolean root
# Empty structures
assert solution.jsonStringify([]) == "[]" # empty array
assert solution.jsonStringify({}) == "{}" # empty object
# Primitive roots
assert solution.jsonStringify(None) == "null" # null root
assert solution.jsonStringify(123) == "123" # integer root
assert solution.jsonStringify("abc") == '"abc"' # string root
# Nested arrays
assert solution.jsonStringify(
[1, [2, [3]], 4]
) == '[1,[2,[3]],4]' # deeply nested arrays
# Nested objects
assert solution.jsonStringify(
{"a": {"b": {"c": {"d": 1}}}}
) == '{"a":{"b":{"c":{"d":1}}}}' # deeply nested objects
# Mixed nesting
assert solution.jsonStringify(
[{"a": [1, 2, {"b": False}]}]
) == '[{"a":[1,2,{"b":false}]}]' # object inside array
# Boolean handling
assert solution.jsonStringify(False) == "false" # false literal
# Array with nulls
assert solution.jsonStringify(
[None, None]
) == '[null,null]' # repeated nulls
| Test | Why |
|---|---|
{"y":1,"x":2} |
Validates object serialization and key ordering |
| Mixed primitive object | Validates all primitive JSON types |
| Nested structures | Confirms recursive traversal works |
true root |
Ensures primitive roots are allowed |
[] |
Validates empty array formatting |
{} |
Validates empty object formatting |
null root |
Ensures null handling is correct |
| Deeply nested arrays | Tests recursive depth handling |
| Deeply nested objects | Tests recursive object traversal |
| Mixed nesting | Ensures arrays and objects interact correctly |
false |
Confirms lowercase boolean formatting |
[null,null] |
Verifies repeated primitive serialization |
Edge Cases
Null Values
null is particularly tricky because in JavaScript, typeof null returns "object". A naive implementation that checks objects first could incorrectly attempt object traversal on null. The implementation avoids this by explicitly checking for None or null before object handling.
Boolean Type Ordering
In Python, bool inherits from int. This means:
isinstance(True, int)
returns True.
If the implementation checked integers before booleans, the output would incorrectly become "True" instead of the required JSON literal "true". The solution handles booleans first to avoid this bug.
Empty Arrays and Objects
Empty collections often expose delimiter bugs. A naive implementation might accidentally produce:
[,]
or:
{,}
The solution avoids this issue by collecting serialized pieces into arrays and using ",".join(...). Joining an empty list correctly produces an empty string, resulting in valid outputs [] and {}.