955 Json Value
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
JsonValue with six variants: Null, Bool, Number, Str, Array, Objectis_null, is_bool, is_number, etc. using the matches! macroto_string_simple with pattern matching that handles each variant including integer/float formatting for NumberJsonValue::object(&[(&str, JsonValue)]), JsonValue::array(&[JsonValue])Vec<JsonValue> ↔ json list, Vec<(String, JsonValue)> ↔ (string * json) listCode 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
| Aspect | Rust | OCaml |
|---|---|---|
| Recursive enum | Vec<JsonValue> in variant | json list in variant |
#[derive(Clone)] | Explicit; Vec requires Clone | Automatic structural sharing via GC |
matches! macro | Pattern predicate shorthand | function shorthand |
| String vs str | String (owned) for keys | string always owned |
| Ordered object | Vec<(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");
}
}
}#[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 typematch exhaustively handles all cases=) works automatically for all typesof (string * json) list naturally models JSON objects as association listsRust Approach
enum JsonValue { Null, Bool(bool), ... } — direct enum translationBox<T> not needed here since Vec provides indirection for recursion#[derive(Debug, Clone, PartialEq)] adds traits OCaml has by defaultmatches! macro for concise type-checking predicatesString (owned), not &str (borrowed), for owned dataComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Type definition | type json = Null \| Bool of bool \| ... | enum JsonValue { Null, Bool(bool), ... } |
| Structural equality | Built-in = | #[derive(PartialEq)] |
| List type | json list | Vec<JsonValue> |
| String type | string | String |
| Object representation | (string * json) list | Vec<(String, JsonValue)> |
| Pattern matching | match j with \| Null -> ... | match self { JsonValue::Null => ... } |
| Recursive types | Implicit | Implicit (via Vec/Box) |
| Clone | let j2 = j (GC copies) | #[derive(Clone)] explicit |
Exercises
get(key: &str) on Object variants that returns Option<&JsonValue>.index(i: usize) on Array variants that returns Option<&JsonValue>.From<bool>, From<f64>, From<&str>, From<i64> trait implementations for ergonomic construction.deep_equal(a: &JsonValue, b: &JsonValue) -> bool without using PartialEq.flatten_arrays that replaces nested arrays with flat ones recursively.