ExamplesBy LevelBy TopicLearning Paths
955 Fundamental

955 Json Value

Functional Programming

Tutorial

The Problem

Represent JSON values as a recursive Rust enum, mirroring OCaml's algebraic type type json = Null | Bool of bool | Number of float | Str of string | Array of json list | Object of (string * json) list. Implement type predicates, a simple string serializer, and builder helpers. This exercise demonstrates how ADTs model recursive data structures and how Rust's enum corresponds exactly to OCaml's variant types.

🎯 Learning Outcomes

  • • Define a recursive enum JsonValue with six variants: Null, Bool, Number, Str, Array, Object
  • • Implement is_null, is_bool, is_number, etc. using the matches! macro
  • • Implement to_string_simple with pattern matching that handles each variant including integer/float formatting for Number
  • • Build builder helpers: JsonValue::object(&[(&str, JsonValue)]), JsonValue::array(&[JsonValue])
  • • Understand the Rust/OCaml parallel: Vec<JsonValue>json list, Vec<(String, JsonValue)>(string * json) list
  • Code Example

    #![allow(clippy::all)]
    // 955: JSON Value Type
    // OCaml: type json = Null | Bool of bool | Number of float | Str of string | Array of json list | Object of (string * json) list
    // Rust: enum JsonValue with derived traits
    
    // Approach 1: Direct enum translation
    #[derive(Debug, Clone, PartialEq)]
    pub enum JsonValue {
        Null,
        Bool(bool),
        Number(f64),
        Str(String),
        Array(Vec<JsonValue>),
        Object(Vec<(String, JsonValue)>),
    }
    
    // Approach 2: Type checks and simple display
    impl JsonValue {
        pub fn is_null(&self) -> bool {
            matches!(self, JsonValue::Null)
        }
        pub fn is_bool(&self) -> bool {
            matches!(self, JsonValue::Bool(_))
        }
        pub fn is_number(&self) -> bool {
            matches!(self, JsonValue::Number(_))
        }
        pub fn is_string(&self) -> bool {
            matches!(self, JsonValue::Str(_))
        }
        pub fn is_array(&self) -> bool {
            matches!(self, JsonValue::Array(_))
        }
        pub fn is_object(&self) -> bool {
            matches!(self, JsonValue::Object(_))
        }
    
        pub fn to_string_simple(&self) -> String {
            match self {
                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!("\"{}\"", s),
                JsonValue::Array(_) => "[...]".to_string(),
                JsonValue::Object(_) => "{...}".to_string(),
            }
        }
    }
    
    // Approach 3: Builder helpers
    impl JsonValue {
        pub fn object(pairs: &[(&str, JsonValue)]) -> Self {
            JsonValue::Object(
                pairs
                    .iter()
                    .map(|(k, v)| (k.to_string(), v.clone()))
                    .collect(),
            )
        }
    
        pub fn array(items: Vec<JsonValue>) -> Self {
            JsonValue::Array(items)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_type_checks() {
            assert!(JsonValue::Null.is_null());
            assert!(JsonValue::Bool(true).is_bool());
            assert!(JsonValue::Number(1.0).is_number());
            assert!(JsonValue::Str("x".into()).is_string());
            assert!(JsonValue::Array(vec![]).is_array());
            assert!(JsonValue::Object(vec![]).is_object());
        }
    
        #[test]
        fn test_to_string_simple() {
            assert_eq!(JsonValue::Null.to_string_simple(), "null");
            assert_eq!(JsonValue::Bool(true).to_string_simple(), "true");
            assert_eq!(JsonValue::Bool(false).to_string_simple(), "false");
            assert_eq!(JsonValue::Number(42.0).to_string_simple(), "42");
            assert_eq!(
                JsonValue::Str("hello".into()).to_string_simple(),
                "\"hello\""
            );
            assert_eq!(JsonValue::Array(vec![]).to_string_simple(), "[...]");
            assert_eq!(JsonValue::Object(vec![]).to_string_simple(), "{...}");
        }
    
        #[test]
        fn test_equality() {
            assert_eq!(JsonValue::Null, JsonValue::Null);
            assert_eq!(JsonValue::Bool(true), JsonValue::Bool(true));
            assert_ne!(JsonValue::Bool(true), JsonValue::Bool(false));
            assert_eq!(JsonValue::Number(1.0), JsonValue::Number(1.0));
            let arr1 = JsonValue::Array(vec![JsonValue::Null, JsonValue::Bool(true)]);
            let arr2 = JsonValue::Array(vec![JsonValue::Null, JsonValue::Bool(true)]);
            assert_eq!(arr1, arr2);
        }
    
        #[test]
        fn test_nested_object() {
            let obj = JsonValue::object(&[
                ("name", JsonValue::Str("Alice".into())),
                ("age", JsonValue::Number(30.0)),
                ("active", JsonValue::Bool(true)),
            ]);
            assert!(obj.is_object());
            if let JsonValue::Object(pairs) = &obj {
                assert_eq!(pairs.len(), 3);
                assert_eq!(pairs[0].0, "name");
            }
        }
    }

    Key Differences

    AspectRustOCaml
    Recursive enumVec<JsonValue> in variantjson list in variant
    #[derive(Clone)]Explicit; Vec requires CloneAutomatic structural sharing via GC
    matches! macroPattern predicate shorthandfunction shorthand
    String vs strString (owned) for keysstring always owned
    Ordered objectVec<(String, JsonValue)>(string * json) list

    This type is the foundation for examples 956 (pretty-print), 957 (query), and related JSON manipulation exercises. The ADT approach makes impossible states unrepresentable: a Number always contains a valid f64, an Array always contains valid JsonValue elements.

    OCaml Approach

    type json =
      | Null
      | Bool of bool
      | Number of float
      | Str of string
      | Array of json list
      | Object of (string * json) list
    
    let rec to_string = function
      | Null -> "null"
      | Bool true -> "true"
      | Bool false -> "false"
      | 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" s  (* %S: OCaml string with escaping *)
      | Array _ -> "[...]"
      | Object _ -> "{...}"
    
    let is_null = function Null -> true | _ -> false
    let is_bool = function Bool _ -> true | _ -> false
    

    OCaml's type json and Rust's enum JsonValue are structurally identical. The function keyword in OCaml is shorthand for fun x -> match x with. OCaml's pattern matching and Rust's match have the same power and exhaustiveness checking.

    Full Source

    #![allow(clippy::all)]
    // 955: JSON Value Type
    // OCaml: type json = Null | Bool of bool | Number of float | Str of string | Array of json list | Object of (string * json) list
    // Rust: enum JsonValue with derived traits
    
    // Approach 1: Direct enum translation
    #[derive(Debug, Clone, PartialEq)]
    pub enum JsonValue {
        Null,
        Bool(bool),
        Number(f64),
        Str(String),
        Array(Vec<JsonValue>),
        Object(Vec<(String, JsonValue)>),
    }
    
    // Approach 2: Type checks and simple display
    impl JsonValue {
        pub fn is_null(&self) -> bool {
            matches!(self, JsonValue::Null)
        }
        pub fn is_bool(&self) -> bool {
            matches!(self, JsonValue::Bool(_))
        }
        pub fn is_number(&self) -> bool {
            matches!(self, JsonValue::Number(_))
        }
        pub fn is_string(&self) -> bool {
            matches!(self, JsonValue::Str(_))
        }
        pub fn is_array(&self) -> bool {
            matches!(self, JsonValue::Array(_))
        }
        pub fn is_object(&self) -> bool {
            matches!(self, JsonValue::Object(_))
        }
    
        pub fn to_string_simple(&self) -> String {
            match self {
                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!("\"{}\"", s),
                JsonValue::Array(_) => "[...]".to_string(),
                JsonValue::Object(_) => "{...}".to_string(),
            }
        }
    }
    
    // Approach 3: Builder helpers
    impl JsonValue {
        pub fn object(pairs: &[(&str, JsonValue)]) -> Self {
            JsonValue::Object(
                pairs
                    .iter()
                    .map(|(k, v)| (k.to_string(), v.clone()))
                    .collect(),
            )
        }
    
        pub fn array(items: Vec<JsonValue>) -> Self {
            JsonValue::Array(items)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_type_checks() {
            assert!(JsonValue::Null.is_null());
            assert!(JsonValue::Bool(true).is_bool());
            assert!(JsonValue::Number(1.0).is_number());
            assert!(JsonValue::Str("x".into()).is_string());
            assert!(JsonValue::Array(vec![]).is_array());
            assert!(JsonValue::Object(vec![]).is_object());
        }
    
        #[test]
        fn test_to_string_simple() {
            assert_eq!(JsonValue::Null.to_string_simple(), "null");
            assert_eq!(JsonValue::Bool(true).to_string_simple(), "true");
            assert_eq!(JsonValue::Bool(false).to_string_simple(), "false");
            assert_eq!(JsonValue::Number(42.0).to_string_simple(), "42");
            assert_eq!(
                JsonValue::Str("hello".into()).to_string_simple(),
                "\"hello\""
            );
            assert_eq!(JsonValue::Array(vec![]).to_string_simple(), "[...]");
            assert_eq!(JsonValue::Object(vec![]).to_string_simple(), "{...}");
        }
    
        #[test]
        fn test_equality() {
            assert_eq!(JsonValue::Null, JsonValue::Null);
            assert_eq!(JsonValue::Bool(true), JsonValue::Bool(true));
            assert_ne!(JsonValue::Bool(true), JsonValue::Bool(false));
            assert_eq!(JsonValue::Number(1.0), JsonValue::Number(1.0));
            let arr1 = JsonValue::Array(vec![JsonValue::Null, JsonValue::Bool(true)]);
            let arr2 = JsonValue::Array(vec![JsonValue::Null, JsonValue::Bool(true)]);
            assert_eq!(arr1, arr2);
        }
    
        #[test]
        fn test_nested_object() {
            let obj = JsonValue::object(&[
                ("name", JsonValue::Str("Alice".into())),
                ("age", JsonValue::Number(30.0)),
                ("active", JsonValue::Bool(true)),
            ]);
            assert!(obj.is_object());
            if let JsonValue::Object(pairs) = &obj {
                assert_eq!(pairs.len(), 3);
                assert_eq!(pairs[0].0, "name");
            }
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_type_checks() {
            assert!(JsonValue::Null.is_null());
            assert!(JsonValue::Bool(true).is_bool());
            assert!(JsonValue::Number(1.0).is_number());
            assert!(JsonValue::Str("x".into()).is_string());
            assert!(JsonValue::Array(vec![]).is_array());
            assert!(JsonValue::Object(vec![]).is_object());
        }
    
        #[test]
        fn test_to_string_simple() {
            assert_eq!(JsonValue::Null.to_string_simple(), "null");
            assert_eq!(JsonValue::Bool(true).to_string_simple(), "true");
            assert_eq!(JsonValue::Bool(false).to_string_simple(), "false");
            assert_eq!(JsonValue::Number(42.0).to_string_simple(), "42");
            assert_eq!(
                JsonValue::Str("hello".into()).to_string_simple(),
                "\"hello\""
            );
            assert_eq!(JsonValue::Array(vec![]).to_string_simple(), "[...]");
            assert_eq!(JsonValue::Object(vec![]).to_string_simple(), "{...}");
        }
    
        #[test]
        fn test_equality() {
            assert_eq!(JsonValue::Null, JsonValue::Null);
            assert_eq!(JsonValue::Bool(true), JsonValue::Bool(true));
            assert_ne!(JsonValue::Bool(true), JsonValue::Bool(false));
            assert_eq!(JsonValue::Number(1.0), JsonValue::Number(1.0));
            let arr1 = JsonValue::Array(vec![JsonValue::Null, JsonValue::Bool(true)]);
            let arr2 = JsonValue::Array(vec![JsonValue::Null, JsonValue::Bool(true)]);
            assert_eq!(arr1, arr2);
        }
    
        #[test]
        fn test_nested_object() {
            let obj = JsonValue::object(&[
                ("name", JsonValue::Str("Alice".into())),
                ("age", JsonValue::Number(30.0)),
                ("active", JsonValue::Bool(true)),
            ]);
            assert!(obj.is_object());
            if let JsonValue::Object(pairs) = &obj {
                assert_eq!(pairs.len(), 3);
                assert_eq!(pairs[0].0, "name");
            }
        }
    }

    Deep Comparison

    JSON Value Type — Comparison

    Core Insight

    Both OCaml and Rust model JSON as a recursive algebraic data type. The mapping is nearly 1:1: OCaml variants become Rust enum variants, list becomes Vec, and string becomes String. The key difference is Rust requires explicit PartialEq derivation while OCaml's structural equality is built-in.

    OCaml Approach

  • type json = Null | Bool of bool | ... — recursive variant type
  • • Pattern matching with match exhaustively handles all cases
  • • Structural equality (=) works automatically for all types
  • of (string * json) list naturally models JSON objects as association lists
  • • Recursive types require no special annotation (OCaml handles it)
  • Rust Approach

  • enum JsonValue { Null, Bool(bool), ... } — direct enum translation
  • Box<T> not needed here since Vec provides indirection for recursion
  • #[derive(Debug, Clone, PartialEq)] adds traits OCaml has by default
  • matches! macro for concise type-checking predicates
  • • Strings are String (owned), not &str (borrowed), for owned data
  • Comparison Table

    AspectOCamlRust
    Type definitiontype json = Null \| Bool of bool \| ...enum JsonValue { Null, Bool(bool), ... }
    Structural equalityBuilt-in =#[derive(PartialEq)]
    List typejson listVec<JsonValue>
    String typestringString
    Object representation(string * json) listVec<(String, JsonValue)>
    Pattern matchingmatch j with \| Null -> ...match self { JsonValue::Null => ... }
    Recursive typesImplicitImplicit (via Vec/Box)
    Clonelet j2 = j (GC copies)#[derive(Clone)] explicit

    Exercises

  • Implement get(key: &str) on Object variants that returns Option<&JsonValue>.
  • Implement index(i: usize) on Array variants that returns Option<&JsonValue>.
  • Add From<bool>, From<f64>, From<&str>, From<i64> trait implementations for ergonomic construction.
  • Implement deep_equal(a: &JsonValue, b: &JsonValue) -> bool without using PartialEq.
  • Implement flatten_arrays that replaces nested arrays with flat ones recursively.
  • Open Source Repos