The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

fastgeojson

High-performance GeoJSON and JSON serialization for R

fastgeojson converts sf objects to GeoJSON FeatureCollections and generic R objects (data.frame, lists, vectors) to JSON strings.

Implemented in Rust via extendr, it writes lossless numbers, runs in parallel, and delivers 6–27× speedups over jsonlite and geojsonsf on large datasets at equal output, and 2.7–3.2× over yyjsonr.

Results are ready for Shiny, Plumber, leaflet::addGeoJSON(), and any package that talks to JavaScript.

Status: v0.3.0as_json() takes jsonlite::toJSON()’s arguments, in the same order, and follows its output conventions; two defaults differ: numbers are lossless, and sf objects become GeoJSON. See Upgrading.

Performance

Fastest of 7 runs, every package writing lossless numbers (jsonlite at digits = I(17), the others at their defaults) and the same JSON. Reproduce with Rscript tools/bench/readme_bench.R.

1 million rows × 4 mixed columns

Package Time_ms Output_MB Speedup vs jsonlite
jsonlite 1945 61.6
jsonify 1293 61.1 1.5×
yyjsonr 233 61.1 8.3×
fastgeojson (1 thread) 148 61.1 13.1×
fastgeojson 73 61.1 26.6×

1 million point features

Package Time_ms Output_MB Speedup vs geojsonsf
geojsonsf 1858 150.8
yyjsonr 573 150.8 3.2×
fastgeojson (1 thread) 327 150.8 5.7×
fastgeojson 176 150.8 10.6×

10,000 polygons × 200 vertices

Package Time_ms Output_MB Speedup vs geojsonsf
geojsonsf 597 76.4
yyjsonr 245 76.4 2.4×
fastgeojson (1 thread) 196 76.4 3.0×
fastgeojson 92 76.4 6.5×

jsonlite’s own default rounds to 4 decimal places, which is faster for it (1.1 s here) and 20% smaller; as_json(x, digits = 4) reproduces that output. Single-threaded, fastgeojson is 1.25–1.8× faster than yyjsonr.

Most of each call is R interning the result as a string, at about a nanosecond per byte. as_bytes = TRUE returns the bytes without it:

full call as_bytes = TRUE R’s share
1M rows × 4 columns 72.1 ms 11.7 ms 84%
1M point features 176.8 ms 22.3 ms 87%
10k polygons × 200 vertices 91.5 ms 13.3 ms 85%

Correctness

Output follows jsonlite’s conventions: jsonlite’s own toJSON test suite (jsonlite 2.0.0, in tests/testthat/ with toJSON() bound to as_json()) passes with no failures and no skips, including its sf tests against GDAL’s GeoJSON writer.

Two defaults differ. jsonlite rounds numbers to 4 decimal places; as_json() writes the shortest decimal that reads back as the same double (digits = 4 gives toJSON()’s output). And jsonlite writes sf objects as a record array; as_json() writes a FeatureCollection.

as_json(nc)                      # FeatureCollection  (default here)
as_json(nc, sf = "features")     # array of Feature objects
as_json(nc, sf = "dataframe")    # record array       (jsonlite default)

options(fastgeojson.sf = "dataframe")   # or switch the default globally

Upgrading from 0.1.x or 0.2.x

sf_geojson_str() and df_json_str() — the whole API of 0.1.x — are gone; as_json() does both and dispatches on its input. Replace either with as_json(x, ...).

Arguments are positional in jsonlite’s order, so as_json(df, "columns") now means dataframe = "columns". Named arguments are unaffected.

Output at the defaults changes where 0.2.2 differed from toJSON() — measured by serializing the same inputs with both versions:

0.2.2 0.3.0
missing value in a row-oriented frame "d":null key omitted; na = "null" keeps it
bare numeric NA, NaN, Inf null "NA", "NaN", "Inf"; na = "null" keeps null
matrix flattened, [1,2,3,4] nested by row, [[1,3],[2,4]]
control character in a string \u000A \n
FeatureCollection no name "name":"sfdata", as GDAL writes it
whole coordinate 3.0 3

Numbers are otherwise unchanged: 0.2.2 had no digits argument and always wrote them losslessly, which is now the default digits = Inf. Named vectors, "json"-class strings and time zones behave as before — the keep_vec_names, json_verbatim and UTC arguments are new, and their defaults reproduce 0.2.2. So are Date and POSIXt, which follow jsonlite: POSIXt = "string" uses format(), "ISO8601" emits "2013-06-17T22:33:44", and Date = "epoch" returns days. Full list in NEWS.md.

Installation

Requires R 4.5 or later.

install.packages("fastgeojson")           # CRAN, once available

Development version and pre-compiled Windows/macOS binaries (no Rust required):

options(repos = c(
  firstzero = "https://firstzeroenergy.r-universe.dev",
  CRAN = "https://cloud.r-project.org"
))
install.packages("fastgeojson")

Deploying to shinyapps.io: the CRAN version works automatically. For the R-universe version, add those same options(repos = ...) lines to the top of app.R or global.R so the build server can find the package.

API

as_json(
  x,
  dataframe = c("rows", "columns", "values"),
  matrix    = c("rowmajor", "columnmajor"),
  Date      = c("ISO8601", "epoch"),
  POSIXt    = c("string", "ISO8601", "epoch", "mongo"),
  factor    = c("string", "integer"),
  complex   = c("string", "list"),
  raw       = c("base64", "hex", "mongo", "int", "js"),
  null      = c("list", "null"),
  na        = c("null", "string"),
  auto_unbox = FALSE,
  digits     = Inf,
  pretty     = FALSE,
  force      = FALSE,
  ...
)

as_json() is the only encoder. It detects the input type and returns a length-one character vector of class "json", or c("geojson", "json") for sf input — or a raw vector with as_bytes = TRUE.

Two options go beyond toJSON():

as_json(x, digits = Inf)      # the default: shortest decimal that round-trips exactly
as_json(x, as_bytes = TRUE)   # a raw vector instead of a character vector

digits: Inf (the default) is lossless; a number is decimal places, as in toJSON(); I(n) is significant digits; NA is toJSON()’s 15 significant digits.

fastgeojson_threads(n) sets the worker count (1 disables parallelism, 0 restores automatic); the default is the whole machine, honouring FASTGEOJSON_NUM_THREADS, RAYON_NUM_THREADS, OMP_NUM_THREADS and OMP_THREAD_LIMIT. Output is identical at any thread count.

as_bytes

Returns the same bytes as a raw vector, skipping the interning that is 84–87% of a large call. Use it when the JSON is leaving R and never needs to be an R string:

con <- file("out.json", "wb")                          # a file
writeBin(as_json(x, as_bytes = TRUE), con); close(con)

res$body <- as_json(x, as_bytes = TRUE)                # httpuv, plumber, shiny
httr2::req_body_raw(req, as_json(x, as_bytes = TRUE), "application/json")
writeBin(as_json(x, as_bytes = TRUE), gzfile("out.json.gz", "wb"))

Writing an 18 MB result to a file takes 9 ms this way, against 101 ms through writeLines() on the character result and 415 ms through jsonlite::write_json(). It cannot be combined with pretty.

For htmlwidgets (leaflet::addGeoJSON(), deckgl, mapdeck) use the default: they splice a "json"-classed string into the payload verbatim, but base64-encode a raw vector.

Usage

library(sf)
library(fastgeojson)

nc <- st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)

json_out <- as_json(nc)
class(json_out)
#> [1] "geojson" "json"

library(leaflet)
leaflet() |> addTiles() |> addGeoJSON(json_out)
df <- data.frame(id = 1:2, name = c("Alice", "Bob"), score = c(98.5, NA))

as_json(df)
#> [{"id":1,"name":"Alice","score":98.5},{"id":2,"name":"Bob"}]

as_json(df, dataframe = "columns")
#> {"id":[1,2],"name":["Alice","Bob"],"score":[98.5,"NA"]}

Row-oriented output omits missing fields; column-oriented output keeps array lengths aligned, as jsonlite does.

as_json(list(val = 5))
#> {"val":[5]}

as_json(list(val = 5), auto_unbox = TRUE)
#> {"val":5}

as_json(list(meta = list(version = "1.0"), payload = c(10, 20)), auto_unbox = TRUE)
#> {"meta":{"version":"1.0"},"payload":[10,20]}

as_json(list(a = 1:2, b = list(c = "x")), pretty = TRUE)
#> {
#>   "a": [1, 2],
#>   "b": {
#>     "c": ["x"]
#>   }
#> }

Because as_json() returns pre-classed json strings, Shiny can hand them to the browser without re-encoding:

observe({
  session$sendCustomMessage("updateMap", as_json(large_sf_object))
})

Supported types

The full jsonlite::toJSON() argument surface is supported.

Implementation

Development

Rust in src/rust/, R interface in R/, benchmarks and parity verifiers in tools/bench/. Built with extendr. FASTGEOJSON_PROFILE=1 prints per-phase timings.

Bug reports, feature requests, and contributions are very welcome.

License

MIT © FirstZero Energy

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.