956 Json Pretty Print
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
pretty_print(json: &JsonValue, indent: usize) -> String recursively with depth tracking" ".repeat(indent * 2) for current-level padding and (indent + 1) * 2 for child paddingescape_string for JSON-safe string output: \", \\, \n, \t, \rNumber variants as integers when fract() == 0.0 && is_finite()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
| Aspect | Rust | OCaml |
|---|---|---|
| Default argument | Separate public wrapper or always pass | ?(indent=0) optional argument |
| String building | format! + String::with_capacity | Printf.sprintf + ^ concatenation |
| String joining | .join(",\n") | String.concat ",\n" |
| Escaping | Match per character, push to String | Same 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(']'));
}
}#[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
?(indent=0) gives default indentation cleanlyString.make n ' ' builds padding stringsString.concat joins lists of strings with separatorBuffer used for escape_string to avoid O(n²) string concatenationRust Approach
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.concatformat! for string interpolation instead of Printf.sprintfif items.is_empty() guard before match arm (or use pattern guard)Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| String building | Buffer.add_string / ^ | String::push_str / format! |
| Joining list | String.concat sep list | vec.join(sep) |
| Default args | ?(indent=0) | No default args ā overload or wrapper |
| Padding | String.make n ' ' | " ".repeat(n) |
| Char escaping | String.iter + match c | for c in s.chars() + match c |
| Empty collection | Pattern Array [] | Pattern guard if items.is_empty() |
| Float formatting | Printf.sprintf "%g" | format!("{}", n) |
Exercises
to_json_compact with no indentation or newlines.\uXXXX.max_depth parameter and return Err if the JSON nests deeper than the limit.diff(a: &JsonValue, b: &JsonValue) that prints which values differ between two JSON trees.