a-b-street/abstreet · error
Couldn't read_json( )
Error message
Couldn't read_json({}): {} What it means
abstio::read_json is an infallible wrapper around maybe_read_json: it deserializes a JSON file at the given path, and if the read or parse fails (missing file, bad permissions, malformed JSON, schema mismatch) it panics with the path and underlying error. It exists for cases where the caller considers the file mandatory.
Solutions
- Verify the file exists at the exact resolved path (print the path; check with std::path::Path::exists or abstio::file_exists)
- Switch to abstio::maybe_read_json and handle the Err gracefully instead of panicking when the file may legitimately be absent
- Regenerate or re-download the data (run the importer / data sync) if the file is missing because data wasn't built
- Validate the JSON against the expected schema / check serde version compatibility if the error is a parse or deserialization failure
Example fix
// before
let edits: EditMap = abstio::read_json(path, timer); // panics if missing
// after
let edits: EditMap = match abstio::maybe_read_json(path.clone(), timer) {
Ok(obj) => obj,
Err(err) => {
eprintln!("optional file {} unavailable: {}", path, err);
EditMap::default()
}
}; Defensive patterns
Strategy: try-catch
Validate before calling
if !abstio::file_exists(path.clone()) {
eprintln!("required file missing: {}", path);
return;
}
let data: T = abstio::read_json(path, timer); Try / catch
// Prefer the fallible API instead of catching the panic:
match abstio::maybe_read_json::<T>(path.clone(), timer) {
Ok(obj) => obj,
Err(err) => {
eprintln!("Couldn't read {}: {}", path, err);
Default::default() // or propagate the error
}
} Prevention
- Use maybe_read_json for any file that may legitimately be absent; reserve read_json for mandatory files
- Log/print the resolved path so missing-file panics are easy to diagnose
- Ensure data has been imported/downloaded before loading input or system files
- Regenerate JSON after schema changes so serialized files match current struct versions
When it happens
Trigger: Any call to abstio::read_json::<T>(path, timer) where maybe_read_json fails: the file does not exist at the resolved path, the JSON is syntactically invalid, or the JSON does not match T's serde expectations. Commonly via wrappers like CityName::input_path-based loads of input/system files.
Common situations: Typo in the path or scenario/map name, calling read_json on an optional file that hasn't been generated yet, running before data has been imported/downloaded, hand-edited JSON with a syntax error, or deserializing an older/newer file version into a changed struct.
Understand the failure class
Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.
Related errors
AI-assisted analysis of a-b-street/abstreet@0964f29315 (2026-09-13).
Data as JSON: /api/errors/f37f7c30a84ec19e.
Report an issue: GitHub.
Appendix: source
Thrown at abstio/src/io.rs:26
use crate::{list_dir, maybe_read_binary, slurp_file};
pub fn maybe_read_json<T: DeserializeOwned>(path: String, timer: &mut Timer) -> Result<T> {
if !path.ends_with(".json") && !path.ends_with(".geojson") {
bail!("read_json needs {} to end with .json or .geojson", path);
}
timer.start(format!("parse {}", path));
// TODO timer.read_file isn't working here. And we need to call stop() if there's no file.
let result: Result<T> =
slurp_file(&path).and_then(|raw| serde_json::from_slice(&raw).map_err(|err| err.into()));
timer.stop(format!("parse {}", path));
result
}
pub fn read_json<T: DeserializeOwned>(path: String, timer: &mut Timer) -> T {
match maybe_read_json(path.clone(), timer) {
Ok(obj) => obj,
Err(err) => panic!("Couldn't read_json({}): {}", path, err),
}
}
pub fn read_binary<T: DeserializeOwned>(path: String, timer: &mut Timer) -> T {
match maybe_read_binary(path.clone(), timer) {
Ok(obj) => obj,
Err(err) => panic!("Couldn't read_binary({}): {}", path, err),
}
}
/// May be a JSON or binary file
pub fn read_object<T: DeserializeOwned>(path: String, timer: &mut Timer) -> Result<T> {
if path.ends_with(".bin") {
maybe_read_binary(path, timer)
} else {
maybe_read_json(path, timer)
}
}View on GitHub (pinned to 0964f29315)