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.

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.0 —
as_json()takesjsonlite::toJSON()’s arguments, in the same order, and follows its output conventions; two defaults differ: numbers are lossless, andsfobjects become GeoJSON. See Upgrading.
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.
| 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× |
| 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× |
| 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% |
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 globallysf_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.
Requires R 4.5 or later.
install.packages("fastgeojson") # CRAN, once availableDevelopment 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.
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 vectordigits: 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.
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.
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))
})Date, POSIXt,
bit64::integer64; difftime as its numeric
value.toJSON().The full jsonlite::toJSON() argument surface is
supported.
jsonlite’s
modp_dtoa2; digits = Inf uses Żmij.format()’s microseconds.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.
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.