2025, Dec 30 23:00

FlatGeoBuf to GeoJSON in Memory with GDAL/OGR VSIMEM: Correct Sequence Using ogr.Open Instead of CreateDataSource in Python

Learn how to convert FlatGeoBuf bytes to GeoJSON in-memory using GDAL/OGR VSIMEM: write first, then open with ogr.Open—not CreateDataSource. Disk-free Python.

Converting a FlatGeoBuf payload received over HTTP directly to GeoJSON without touching disk is a perfectly valid in-memory workflow with GDAL/OGR. The stumbling block is usually the order of operations with VSIMEM and which OGR API to call for reading versus creating data sources.

Reproducing the issue

The pattern that fails looks correct at first glance but initializes the dataset before anything exists at the VSIMEM path. After that, the dataset appears empty because OGR was asked to create a new file instead of opening the one you just wrote in memory.

from osgeo import gdal, ogr
def stash_to_vsimem(mem_uri: str, payload: bytes) -> None:
    handle = gdal.VSIFOpenL(mem_uri, "w")
    size_bytes = len(payload)
    try:
        gdal.VSIFWriteL(payload, 1, size_bytes, handle)
        if gdal.VSIFCloseL(handle) != 0:
            raise RuntimeError(
                f"Failed to close VSIMEM file '{mem_uri}': {gdal.GetLastErrorMsg()}"
            )
    except Exception as exc:
        raise RuntimeError(f"Error writing to VSIMEM file '{mem_uri}': {exc}") from exc
def fgb_bytes_to_geojson(blob: bytes, tag: str) -> bytes:
    drv = ogr.GetDriverByName("FlatGeobuf")
    vpath = f"/vsimem/{tag}.fgb"
    ds = drv.CreateDataSource(vpath)
    if not ds:
        raise RuntimeError(f"Failed to open FlatGeobuf '{tag}' with VSIMEM.")
    stash_to_vsimem(vpath, blob)
    if ds.GetLayerCount() != 1:
        raise ValueError(
            f"Expected 1 layer in FlatGeobuf '{tag}', found {ds.GetLayerCount()} layers."
        )
    lyr = ds.GetLayer(0)
    return layer_to_geojson(layer=lyr)

What actually goes wrong

The core problem is the sequence. Writing to a VSIMEM path creates the in-memory file. Calling CreateDataSource beforehand asks OGR to create a new, empty dataset at that location. Because the bytes were written after the dataset handle was created, OGR still sees an empty container and reports zero layers.

There is a second, closely related nuance. CreateDataSource is intended for creating new files. To read an existing dataset, including one you just wrote to VSIMEM, the correct call is Open. When you invert those two steps, OGR can discover the FlatGeoBuf structure and enumerate layers as expected.

The fix

The solution is straightforward. First, write the byte payload to a VSIMEM path. Then, open that path using ogr.Open and proceed as you would with a file on disk.

from osgeo import gdal, ogr
def write_vmem(uri: str, content: bytes) -> None:
    fh = gdal.VSIFOpenL(uri, "w")
    length = len(content)
    try:
        gdal.VSIFWriteL(content, 1, length, fh)
        if gdal.VSIFCloseL(fh) != 0:
            raise RuntimeError(
                f"Failed to close VSIMEM file '{uri}': {gdal.GetLastErrorMsg()}"
            )
    except Exception as err:
        raise RuntimeError(f"Error writing to VSIMEM file '{uri}': {err}") from err
def flatgeobuf_to_geojson_mem(raw_fgb: bytes, label: str) -> bytes:
    vmem_path = f"/vsimem/{label}.fgb"
    write_vmem(vmem_path, raw_fgb)
    ds = ogr.Open(vmem_path)
    if not ds:
        raise RuntimeError(f"Failed to open FlatGeobuf '{label}' with VSIMEM.")
    if ds.GetLayerCount() != 1:
        raise ValueError(
            f"Expected 1 layer in FlatGeobuf '{label}', found {ds.GetLayerCount()} layers."
        )
    layer = ds.GetLayer(0)
    return layer_to_geojson(layer=layer, layer_name=label)

Why this matters

Understanding how VSIMEM integrates with GDAL/OGR unlocks efficient streaming workflows. You can ingest bytes from an HTTP response, construct a virtual file entirely in memory, and use the standard OGR reading path without allocating temporary files. This keeps IO minimal and avoids filesystem dependencies in containerized or serverless environments.

For other in-memory patterns in Python, file-like objects such as io.BytesIO and io.StringIO provide generic interfaces documented at https://docs.python.org/3/library/io.html. In this case, the path-based approach with VSIMEM aligns directly with how GDAL/OGR expects to read datasets.

Final notes

Write the FlatGeoBuf bytes to a VSIMEM path first, then open that path with ogr.Open. Avoid CreateDataSource when your goal is to read existing content. With that sequence in place, layer discovery and GeoJSON export work the same as with on-disk data, but without touching the filesystem.