ExamplesBy LevelBy TopicLearning Paths
956 Fundamental

956 Json Pretty Print

Functional Programming

Tutorial

The Problem

Implement a recursive JSON pretty-printer that produces indented, human-readable output from a JsonValue tree. Arrays and objects expand across multiple lines with consistent 2-space indentation per level. Handle string escaping (quotes, backslashes, newlines, tabs) and integer vs float formatting for numbers.

🎯 Learning Outcomes

  • • Implement pretty_print(json: &JsonValue, indent: usize) -> String recursively with depth tracking
  • • Use " ".repeat(indent * 2) for current-level padding and (indent + 1) * 2 for child padding
  • • Implement escape_string for JSON-safe string output: \", \\, \n, \t, \r
  • • Format Number variants as integers when fract() == 0.0 && is_finite()
  • • Produce compact (no trailing newline, proper comma placement) multi-line output
  • Code Example

    #![allow(clippy::all)]
    // 956: JSON Pretty Print
    // Recursive pretty-printer: OCaml uses Buffer, Rust builds String
    
    #[derive(Debug, Clone, PartialEq)]
    pub enum JsonValue {
        Null,
        Bool(bool),
        Number(f64),
        Str(String),
        Array(Vec<JsonValue>),
        Object(Vec<(String, JsonValue)>),
    }
    
    // Approach 1: Pretty-print with indentation (recursive, builds String)
    fn escape_string(s: &str) -> String {
        let mut out = String::with_capacity(s.len());
        for c in s.chars() {
            match c {
                '"' => out.push_str("\\\""),
                '\\' => out.push_str("\\\\"),
                '\n' => out.push_str("\\n"),
                '\t' => out.push_str("\\t"),
                '\r' => out.push_str("\\r"),
                c => out.push(c),
            }
        }
        out
    }
    
    fn pretty_print(j: &JsonValue, indent: usize) -> String {
        let pad = " ".repeat(indent * 2);
        let pad2 = " ".repeat((indent + 1) * 2);
        match j {
            JsonValue::Null => "null".to_string(),
            JsonValue::Bool(true) => "true".to_string(),
            JsonValue::Bool(false) => "false".to_string(),
            JsonValue::Number(n) => {
                if n.fract() == 0.0 && n.is_finite() {
                    format!("{}", *n as i64)
                } else {
                    format!("{}", n)
                }
            }
            JsonValue::Str(s) => format!("\"{}\"", escape_string(s)),
            JsonValue::Array(items) if items.is_empty() => "[]".to_string(),
            JsonValue::Array(items) => {
                let inner: Vec<String> = items
                    .iter()
                    .map(|item| format!("{}{}", pad2, pretty_print(item, indent + 1)))
                    .collect();
                format!("[\n{}\n{}]", inner.join(",\n"), pad)
            }
            JsonValue::Object(pairs) if pairs.is_empty() => "{}".to_string(),
            JsonValue::Object(pairs) => {
                let inner: Vec<String> = pairs
                    .iter()
                    .map(|(k, v)| {
                        format!(
                            "{}\"{}\": {}",
                            pad2,
                            escape_string(k),
                            pretty_print(v, indent + 1)
                        )
                    })
                    .collect();
                format!("{{\n{}\n{}}}", inner.join(",\n"), pad)
            }
        }
    }
    
    // Approach 2: Compact (single-line) printer
    fn compact(j: &JsonValue) -> String {
        match j {
            JsonValue::Null => "null".to_string(),
            JsonValue::Bool(b) => b.to_string(),
            JsonValue::Number(n) => {
                if n.fract() == 0.0 && n.is_finite() {
                    format!("{}", *n as i64)
                } else {
                    format!("{}", n)
                }
            }
            JsonValue::Str(s) => format!("\"{}\"", escape_string(s)),
            JsonValue::Array(items) => {
                let inner: Vec<String> = items.iter().map(compact).collect();
                format!("[{}]", inner.join(","))
            }
            JsonValue::Object(pairs) => {
                let inner: Vec<String> = pairs
                    .iter()
                    .map(|(k, v)| format!("\"{}\":{}", escape_string(k), compact(v)))
                    .collect();
                format!("{{{}}}", inner.join(","))
            }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_primitives() {
            assert_eq!(pretty_print(&JsonValue::Null, 0), "null");
            assert_eq!(pretty_print(&JsonValue::Bool(true), 0), "true");
            assert_eq!(pretty_print(&JsonValue::Bool(false), 0), "false");
            assert_eq!(pretty_print(&JsonValue::Number(42.0), 0), "42");
            assert_eq!(pretty_print(&JsonValue::Str("hi".into()), 0), "\"hi\"");
        }
    
        #[test]
        fn test_escape() {
            let s = JsonValue::Str("hello \"world\"\nnewline".to_string());
            assert_eq!(pretty_print(&s, 0), "\"hello \\\"world\\\"\\nnewline\"");
        }
    
        #[test]
        fn test_empty_array_object() {
            assert_eq!(pretty_print(&JsonValue::Array(vec![]), 0), "[]");
            assert_eq!(pretty_print(&JsonValue::Object(vec![]), 0), "{}");
        }
    
        #[test]
        fn test_compact_no_newlines() {
            let json = JsonValue::Object(vec![
                ("a".to_string(), JsonValue::Number(1.0)),
                ("b".to_string(), JsonValue::Bool(false)),
            ]);
            let c = compact(&json);
            assert!(!c.contains('\n'));
            assert!(c.contains("\"a\":1"));
            assert!(c.contains("\"b\":false"));
        }
    
        #[test]
        fn test_nested_pretty() {
            let json = JsonValue::Array(vec![
                JsonValue::Number(1.0),
                JsonValue::Array(vec![JsonValue::Number(2.0), JsonValue::Number(3.0)]),
            ]);
            let p = pretty_print(&json, 0);
            assert!(p.contains('\n'));
            assert!(p.starts_with('['));
            assert!(p.ends_with(']'));
        }
    }

    Key Differences

    AspectRustOCaml
    Default argumentSeparate public wrapper or always pass?(indent=0) optional argument
    String buildingformat! + String::with_capacityPrintf.sprintf + ^ concatenation
    String joining.join(",\n")String.concat ",\n"
    EscapingMatch per character, push to StringSame approach with Buffer or char match

    The recursive pretty-printer naturally matches the recursive structure of JsonValue. There is no stack depth concern for typical JSON documents; deeply nested structures (depth > 10,000) could overflow the call stack.

    OCaml Approach

    let rec pretty_print ?(indent=0) j =
      let pad  = String.make (indent * 2) ' ' in
      let pad2 = String.make ((indent + 1) * 2) ' ' in
      match j with
      | Null -> "null"
      | Bool b -> string_of_bool b
      | Number n ->
        if Float.is_integer n then string_of_int (int_of_float n)
        else string_of_float n
      | Str s -> Printf.sprintf "\"%s\"" (escape_string s)
      | Array [] -> "[]"
      | Array items ->
        let inner = List.map (fun item ->
          pad2 ^ pretty_print ~indent:(indent+1) item) items in
        Printf.sprintf "[\n%s\n%s]" (String.concat ",\n" inner) pad
      | Object [] -> "{}"
      | Object pairs ->
        let inner = List.map (fun (k, v) ->
          Printf.sprintf "%s\"%s\": %s" pad2 k (pretty_print ~indent:(indent+1) v)) pairs in
        Printf.sprintf "{\n%s\n%s}" (String.concat ",\n" inner) pad
    

    OCaml's optional argument ?(indent=0) provides a default value — cleaner than Rust's single required indent parameter. The structure is otherwise identical.

    Full Source

    #![allow(clippy::all)]
    // 956: JSON Pretty Print
    // Recursive pretty-printer: OCaml uses Buffer, Rust builds String
    
    #[derive(Debug, Clone, PartialEq)]
    pub enum JsonValue {
        Null,
        Bool(bool),
        Number(f64),
        Str(String),
        Array(Vec<JsonValue>),
        Object(Vec<(String, JsonValue)>),
    }
    
    // Approach 1: Pretty-print with indentation (recursive, builds String)
    fn escape_string(s: &str) -> String {
        let mut out = String::with_capacity(s.len());
        for c in s.chars() {
            match c {
                '"' => out.push_str("\\\""),
                '\\' => out.push_str("\\\\"),
                '\n' => out.push_str("\\n"),
                '\t' => out.push_str("\\t"),
                '\r' => out.push_str("\\r"),
                c => out.push(c),
            }
        }
        out
    }
    
    fn pretty_print(j: &JsonValue, indent: usize) -> String {
        let pad = " ".repeat(indent * 2);
        let pad2 = " ".repeat((indent + 1) * 2);
        match j {
            JsonValue::Null => "null".to_string(),
            JsonValue::Bool(true) => "true".to_string(),
            JsonValue::Bool(false) => "false".to_string(),
            JsonValue::Number(n) => {
                if n.fract() == 0.0 && n.is_finite() {
                    format!("{}", *n as i64)
                } else {
                    format!("{}", n)
                }
            }
            JsonValue::Str(s) => format!("\"{}\"", escape_string(s)),
            JsonValue::Array(items) if items.is_empty() => "[]".to_string(),
            JsonValue::Array(items) => {
                let inner: Vec<String> = items
                    .iter()
                    .map(|item| format!("{}{}", pad2, pretty_print(item, indent + 1)))
                    .collect();
                format!("[\n{}\n{}]", inner.join(",\n"), pad)
            }
            JsonValue::Object(pairs) if pairs.is_empty() => "{}".to_string(),
            JsonValue::Object(pairs) => {
                let inner: Vec<String> = pairs
                    .iter()
                    .map(|(k, v)| {
                        format!(
                            "{}\"{}\": {}",
                            pad2,
                            escape_string(k),
                            pretty_print(v, indent + 1)
                        )
                    })
                    .collect();
                format!("{{\n{}\n{}}}", inner.join(",\n"), pad)
            }
        }
    }
    
    // Approach 2: Compact (single-line) printer
    fn compact(j: &JsonValue) -> String {
        match j {
            JsonValue::Null => "null".to_string(),
            JsonValue::Bool(b) => b.to_string(),
            JsonValue::Number(n) => {
                if n.fract() == 0.0 && n.is_finite() {
                    format!("{}", *n as i64)
                } else {
                    format!("{}", n)
                }
            }
            JsonValue::Str(s) => format!("\"{}\"", escape_string(s)),
            JsonValue::Array(items) => {
                let inner: Vec<String> = items.iter().map(compact).collect();
                format!("[{}]", inner.join(","))
            }
            JsonValue::Object(pairs) => {
                let inner: Vec<String> = pairs
                    .iter()
                    .map(|(k, v)| format!("\"{}\":{}", escape_string(k), compact(v)))
                    .collect();
                format!("{{{}}}", inner.join(","))
            }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_primitives() {
            assert_eq!(pretty_print(&JsonValue::Null, 0), "null");
            assert_eq!(pretty_print(&JsonValue::Bool(true), 0), "true");
            assert_eq!(pretty_print(&JsonValue::Bool(false), 0), "false");
            assert_eq!(pretty_print(&JsonValue::Number(42.0), 0), "42");
            assert_eq!(pretty_print(&JsonValue::Str("hi".into()), 0), "\"hi\"");
        }
    
        #[test]
        fn test_escape() {
            let s = JsonValue::Str("hello \"world\"\nnewline".to_string());
            assert_eq!(pretty_print(&s, 0), "\"hello \\\"world\\\"\\nnewline\"");
        }
    
        #[test]
        fn test_empty_array_object() {
            assert_eq!(pretty_print(&JsonValue::Array(vec![]), 0), "[]");
            assert_eq!(pretty_print(&JsonValue::Object(vec![]), 0), "{}");
        }
    
        #[test]
        fn test_compact_no_newlines() {
            let json = JsonValue::Object(vec![
                ("a".to_string(), JsonValue::Number(1.0)),
                ("b".to_string(), JsonValue::Bool(false)),
            ]);
            let c = compact(&json);
            assert!(!c.contains('\n'));
            assert!(c.contains("\"a\":1"));
            assert!(c.contains("\"b\":false"));
        }
    
        #[test]
        fn test_nested_pretty() {
            let json = JsonValue::Array(vec![
                JsonValue::Number(1.0),
                JsonValue::Array(vec![JsonValue::Number(2.0), JsonValue::Number(3.0)]),
            ]);
            let p = pretty_print(&json, 0);
            assert!(p.contains('\n'));
            assert!(p.starts_with('['));
            assert!(p.ends_with(']'));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_primitives() {
            assert_eq!(pretty_print(&JsonValue::Null, 0), "null");
            assert_eq!(pretty_print(&JsonValue::Bool(true), 0), "true");
            assert_eq!(pretty_print(&JsonValue::Bool(false), 0), "false");
            assert_eq!(pretty_print(&JsonValue::Number(42.0), 0), "42");
            assert_eq!(pretty_print(&JsonValue::Str("hi".into()), 0), "\"hi\"");
        }
    
        #[test]
        fn test_escape() {
            let s = JsonValue::Str("hello \"world\"\nnewline".to_string());
            assert_eq!(pretty_print(&s, 0), "\"hello \\\"world\\\"\\nnewline\"");
        }
    
        #[test]
        fn test_empty_array_object() {
            assert_eq!(pretty_print(&JsonValue::Array(vec![]), 0), "[]");
            assert_eq!(pretty_print(&JsonValue::Object(vec![]), 0), "{}");
        }
    
        #[test]
        fn test_compact_no_newlines() {
            let json = JsonValue::Object(vec![
                ("a".to_string(), JsonValue::Number(1.0)),
                ("b".to_string(), JsonValue::Bool(false)),
            ]);
            let c = compact(&json);
            assert!(!c.contains('\n'));
            assert!(c.contains("\"a\":1"));
            assert!(c.contains("\"b\":false"));
        }
    
        #[test]
        fn test_nested_pretty() {
            let json = JsonValue::Array(vec![
                JsonValue::Number(1.0),
                JsonValue::Array(vec![JsonValue::Number(2.0), JsonValue::Number(3.0)]),
            ]);
            let p = pretty_print(&json, 0);
            assert!(p.contains('\n'));
            assert!(p.starts_with('['));
            assert!(p.ends_with(']'));
        }
    }

    Deep Comparison

    JSON Pretty Print — Comparison

    Core Insight

    Pretty-printing is a classic recursive problem. Both languages follow the same algorithm: recurse into nested structures, track indentation depth, and concatenate output. OCaml tends toward Buffer for efficiency; Rust's String::with_capacity + format! + Vec::join achieves the same result idiomatically.

    OCaml Approach

  • • Optional labeled argument ?(indent=0) gives default indentation cleanly
  • • String.make n ' ' builds padding strings
  • • String.concat joins lists of strings with separator
  • • Buffer used for escape_string to avoid O(n²) string concatenation
  • • Pattern matching handles empty vs non-empty arrays/objects separately
  • Rust Approach

  • • Plain usize parameter for indent (no default args — use wrapper fn if needed)
  • • " ".repeat(n) builds padding strings
  • • .collect::<Vec<String>>() then .join(",\n") mirrors String.concat
  • • format! for string interpolation instead of Printf.sprintf
  • • if items.is_empty() guard before match arm (or use pattern guard)
  • Comparison Table

    AspectOCamlRust
    String buildingBuffer.add_string / ^String::push_str / format!
    Joining listString.concat sep listvec.join(sep)
    Default args?(indent=0)No default args — overload or wrapper
    PaddingString.make n ' '" ".repeat(n)
    Char escapingString.iter + match cfor c in s.chars() + match c
    Empty collectionPattern Array []Pattern guard if items.is_empty()
    Float formattingPrintf.sprintf "%g"format!("{}", n)

    Exercises

  • Add a compact (single-line) serializer to_json_compact with no indentation or newlines.
  • Handle Unicode escaping: replace characters outside ASCII printable range with \uXXXX.
  • Add a max_depth parameter and return Err if the JSON nests deeper than the limit.
  • Implement diff(a: &JsonValue, b: &JsonValue) that prints which values differ between two JSON trees.
  • Add trailing comma suppression for the last element in arrays and objects (some formatters prefer this style).
  • Open Source Repos