Working Code
In Phase 4 projects you'll often save settings, data, and logs to files. Combining std::fs with serde makes JSON-based config files easy to work with.
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
const CONFIG_PATH: &str = "config.json";
#[derive(Debug, Serialize, Deserialize)]
struct AppConfig {
host: String,
port: u16,
debug: bool,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".into(),
port: 8080,
debug: false,
}
}
}
fn load_config<P: AsRef<Path>>(path: P) -> Result<AppConfig, Box<dyn std::error::Error>> {
if !path.as_ref().exists() {
let default = AppConfig::default();
save_config(&default, &path)?;
return Ok(default);
}
let raw = fs::read_to_string(&path)?;
let config = serde_json::from_str(&raw)?;
Ok(config)
}
fn save_config<P: AsRef<Path>>(config: &AppConfig, path: P) -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_string_pretty(config)?;
fs::write(path, json)?;
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut config = load_config(CONFIG_PATH)?;
config.debug = true;
save_config(&config, CONFIG_PATH)?;
println!("Current config: {:?}", config);
Ok(())
}
serde_json::to_string_prettyproduces human-readable JSON.Box<dyn Error>lets you return any error type from a single function.- The "create default config if file doesn't exist" pattern is common in CLI tools.
A Minimal read_to_string / write Example
use std::fs;
fn main() -> std::io::Result<()> {
fs::write("hello.txt", "Hello!\nThis is Rust.")?;
let contents = fs::read_to_string("hello.txt")?;
println!("File contents:\n{contents}");
Ok(())
}
Thanks to ?, error handling stays clean. On failure, Err propagates to the caller; on success, execution continues.
Try It Yourself
- Add a
log_path: Option<String>field toAppConfigand use theserde(default)attribute so the field falls back to a default when missing from the JSON. - Modify
load_configto log a friendly message and return a default when JSON parsing fails. - Try using the
serde_yamlcrate to also support YAML format for saving and loading.
- History store — Define
struct History(Vec<String>). Write functions to save it as JSON on exit and restore it on startup. - Pretty vs compact — Compare file sizes when saving with
to_string(compact) vsto_string_pretty. Summarize when to use each. - Custom error type — Create
enum ConfigErrorand implementFrom<std::io::Error>/From<serde_json::Error>. Refactor load/save to returnResult<T, ConfigError>.
Q1. Which traits must you derive to save AppConfig as JSON?
- A) Copy
- B) Serialize / Deserialize
- C) Debug / Display
- D) Default
Q2. What does fs::read_to_string return?
- A)
String - B)
Result<String, std::io::Error> - C)
Option<String> - D)
&str
Q3. What characterizes serde_json::to_string_pretty?
- A) It serializes to binary format
- B) It generates human-readable JSON with indentation
- C) It requires UTF-16
- D) It never returns an error