a-b-street/abstreet · error

Unexpected geojson

Error message

Unexpected geojson: {:?}

What it means

load_boundary in pick_geofabrik reads a boundary file as GeoJSON and accepts only a single Feature or a FeatureCollection. Any other GeoJson variant (e.g. a bare Geometry like Polygon or GeometryCollection at the top level) hits the catch-all bail with this message, which debug-prints the parsed value.

Solutions

  1. Open the boundary file and ensure the top-level object is {"type": "Feature"...} or {"type": "FeatureCollection", "features": [...]}; wrap a bare geometry in a Feature.
  2. Re-export from the GIS tool as GeoJSON with features (not geometry-only).
  3. If you control the code, add a GeoJson::Geometry arm that wraps the geometry into a Feature before matching.

Example fix

// before
{"type": "Polygon", "coordinates": [[...]]}
// after
{"type": "FeatureCollection", "features": [{"type": "Feature", "properties": {}, "geometry": {"type": "Polygon", "coordinates": [[...]]}}]}
Defensive patterns

Strategy: validation

Validate before calling

let gj: GeoJson = serde_json::from_slice(&bytes)?;
match gj {
    GeoJson::Feature(_) | GeoJson::FeatureCollection(_) => {},
    _ => return Err(anyhow!("boundary must be a Feature or FeatureCollection")),
}

Type guard

fn is_feature_or_collection(gj: &GeoJson) -> bool {
    matches!(gj, GeoJson::Feature(_) | GeoJson::FeatureCollection(_))
}

Try / catch

match load_boundary(path) {
    Ok(poly) => poly,
    Err(e) if e.to_string().contains("Unexpected geojson") => {
        eprintln!("Re-export the boundary as a Feature/FeatureCollection: {e}");
        fallback_poly
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling pick_geofabrik with a boundary path whose file contains top-level GeoJson that is neither a Feature nor a FeatureCollection — e.g. a raw geometry object like {"type": "Polygon", ...} produced by hand-rolled or exported GeoJSON.

Common situations: Hand-editing or exporting boundary GeoJSON from tools (QGIS geometry-only export, geojson.io geometry copy) that emits a bare geometry instead of a Feature/FeatureCollection; passing a wrong file that still parses as valid GeoJSON.

Related errors


AI-assisted analysis of a-b-street/abstreet@0964f29315 (2026-09-13). Data as JSON: /api/errors/61d9dd99287cfef7. Report an issue: GitHub.

Appendix: source

Thrown at importer/src/pick_geofabrik.rs:42

        .min_by_key(|(mp, _)| mp.unsigned_area() as usize)
        .unwrap();

    // Contains some directory structure, like north-america/us/wyoming-latest.osm.pbf or
    // asia/yemen-latest.osm.pbf
    let basename = url
        .strip_prefix("https://download.geofabrik.de/")
        .expect("Geofabrik URLs changed");
    let local = abstio::path_shared_input(format!("geofabrik/{basename}"));

    Ok((url, local))
}

fn load_boundary(path: String) -> Result<geo::Polygon> {
    let gj: GeoJson = abstio::maybe_read_json(path, &mut Timer::throwaway())?;
    let mut features = match gj {
        GeoJson::Feature(feature) => vec![feature],
        GeoJson::FeatureCollection(feature_collection) => feature_collection.features,
        _ => bail!("Unexpected geojson: {:?}", gj),
    };
    if features.len() != 1 {
        bail!("Expected exactly 1 feature");
    }
    let poly: geo::Polygon = features
        .pop()
        .unwrap()
        .geometry
        .take()
        .unwrap()
        .value
        .try_into()
        .unwrap();
    Ok(poly)
}

async fn load_remote_geojson(path: String, url: &str) -> Result<GeoJson> {
    if !abstio::file_exists(&path) {

View on GitHub (pinned to 0964f29315)