This vignette defines the snapshot file format, including how R values are stored in JSON and how uploaded files are included in zip bundles.
The current format version is 1. The version increases only when a
change is incompatible with earlier readers. shinysnap reads older
formats and converts them to the current one. The format
field tracks these package changes. Separately, app$version
records your app’s version so that snap_restore() can pass
it to your migrate function.
The snapshot object
In R, a snapshot is a plain list of class shinysnap:
library(shinysnap)
snap <- snap_unserialize('{
"format": 1,
"app": {"name": "myapp", "version": "2.4.1"},
"created": "2026-09-16T18:22:03Z",
"inputs": {"n": 100, "rate": 0.025},
"values": {"prefs": {"digits": 3}},
"bindings": {"n": "shiny.sliderInput", "rate": "shiny.numberInput"}
}')
str(unclass(snap))
#> List of 9
#> $ format : int 1
#> $ app :List of 2
#> ..$ name : chr "myapp"
#> ..$ version: chr "2.4.1"
#> $ created : chr "2026-09-16T18:22:03Z"
#> $ producer : NULL
#> $ inputs :List of 2
#> ..$ n : int 100
#> ..$ rate: num 0.025
#> $ values :List of 1
#> ..$ prefs:List of 1
#> .. ..$ digits: int 3
#> $ bindings : Named chr [1:2] "shiny.sliderInput" "shiny.numberInput"
#> ..- attr(*, "names")= chr [1:2] "n" "rate"
#> $ attachments: list()
#> $ meta : list()| field | content |
|---|---|
format |
the format version, an integer |
app |
name and version of the app; either may be
NULL
|
created |
when the state was captured, ISO 8601 in UTC |
producer |
versions of shinysnap, shiny, and R that wrote the file |
inputs |
named list of input values, using full ids with any module prefixes |
values |
named list of values saved from the server |
bindings |
named character vector, input id to client binding name |
attachments |
file records, present in bundles only |
meta |
any additional information supplied by the app |
inputs and values hold ordinary R values,
like those returned by input$x. They are converted to the
forms below only when writing a file, and converted back when reading
it.
The JSON file
A file contains one JSON object with the keys above, in that order.
It uses UTF-8, an indent of two spaces, and a newline at the end. Input
ids are written in their order in the snapshot; snap_take()
sorts them. This consistent layout makes changes easier to review in
version control. An object or array is written on one line if it fits in
80 columns, or with one element per line otherwise.
Encoding rules
JSON can represent numbers, strings, booleans, arrays, objects, and
null. For R values that need more information, shinysnap
uses a JSON object with a $type field. These objects are
called typed wrappers below.
NULLisnull.An atomic vector of length one without names is a JSON scalar; longer vectors are arrays. Since R has no scalar type, a scalar and an array of length one read back to the same value. A vector of length zero is a typed wrapper,
{"$type": "<storage>", "value": []}, because a bare[]has no type.Logicals are
trueorfalse. Strings are escaped as required by RFC 8259, with characters outside ASCII kept as UTF-8. Integers use digits without a decimal point. Doubles use the fewest digits that read back to the same value, as given byzmij::format_double(). A finite double always contains a.or ane, so1is an integer and1.0a double.NAinside a vector of length greater than one isnullat that position; the other elements determine the type. A vector that is entirelyNA, including a singleNA, is a typed wrapper withnullvalues, so thatNULL,NA, andNA_character_stay distinct.Infinite values and
NaNuse the strings"inf","-inf", and"NaN"inside adoublewrapper, which is used only when such a value is present.Named atomic vectors are
{"$type": "<storage>", "names": [...], "value": [...]}.-
Dates, times, durations, and factors use these wrappers:
-
Date:{"$type": "Date", "value": ["2024-01-01", null]}. -
POSIXct:{"$type": "POSIXct", "tz": "UTC", "value": ["2024-01-01T10:00:00.000Z"]}. Values are written in the stored time zone and rounded to milliseconds. Thetzkey is omitted if there is no time zone attribute. UTC values use the offsetZ; values using the session’s time zone are formatted in UTC and also useZ. -
difftime:{"$type": "difftime", "units": "secs", "value": [...]}. - Factors:
{"$type": "factor", "levels": [...], "value": ["a", null]}, with"ordered": truefor ordered factors.
-
A matrix or array is
{"$type": "array", "storage": "double", "dim": [2, 3], "dimnames": [["a", "b"], null], "value": [...]}with the values ordered by column, as in R. The reader also accepts"matrix"as the type.A data frame is
{"$type": "data.frame", "nrow": 3, "columns": {"x": ..., "y": ...}}, with a"row.names"array when the row names are not the automatic ones. Tibbles are written as data frames.A list whose names are all present and unique is a JSON object. Any other list (unnamed, partially named, or empty) is
{"$type": "list", "names": [...], "value": [...]}withnamesomitted when absent, except that an unnamed list with at least one element that is not a scalar is written as a JSON array, which reads back as a list.Anything else (environments, functions, S4 and R6 objects, unknown classes) is an error by default. With
unsupported = "rds"it becomes{"$type": "rds", "class": [...], "base64": "..."}(in a bundle,{"$type": "rds", "class": [...], "path": "objects/1-values_fit.rds"}). Reading these requirestrust = TRUE; otherwise they decode toNULLwith one warning that lists them.Attributes other than those listed above are dropped when writing.
Keys starting with $ are reserved.
Examples
cat(snap_serialize(list(inputs = list(
n = 1L, x = 1, sum = 0.1 + 0.2, flag = c(TRUE, NA),
empty = character(0), missing = NA, extremes = c(0.5, Inf),
named = c(a = 1L, b = 2L),
when = as.Date("2024-01-15"),
stamp = as.POSIXct("2024-01-01 10:00:00.5", tz = "UTC"),
level = factor("b", levels = c("a", "b")),
M = matrix(1:6, 2, dimnames = list(c("r1", "r2"), NULL)),
df = data.frame(x = 1:2, y = c("a", "b")),
mixed = list(1, "a"),
nested = list(a = 1, b = list(c = NULL))
))))
#> {
#> "format": 1,
#> "app": {"name": null, "version": null},
#> "created": "2026-09-18T07:05:25Z",
#> "producer": {"shinysnap": "0.1.0", "shiny": "1.14.0", "r": "4.6.1"},
#> "inputs": {
#> "n": 1,
#> "x": 1.0,
#> "sum": 0.30000000000000004,
#> "flag": [true, null],
#> "empty": {"$type": "character", "value": []},
#> "missing": {"$type": "logical", "value": [null]},
#> "extremes": {"$type": "double", "value": [0.5, "inf"]},
#> "named": {"$type": "integer", "names": ["a", "b"], "value": [1, 2]},
#> "when": {"$type": "Date", "value": ["2024-01-15"]},
#> "stamp": {
#> "$type": "POSIXct",
#> "tz": "UTC",
#> "value": ["2024-01-01T10:00:00.500Z"]
#> },
#> "level": {"$type": "factor", "levels": ["a", "b"], "value": ["b"]},
#> "M": {
#> "$type": "array",
#> "storage": "integer",
#> "dim": [2, 3],
#> "dimnames": [["r1", "r2"], null],
#> "value": [1, 2, 3, 4, 5, 6]
#> },
#> "df": {
#> "$type": "data.frame",
#> "nrow": 2,
#> "columns": {"x": [1, 2], "y": ["a", "b"]}
#> },
#> "mixed": {"$type": "list", "value": [1.0, "a"]},
#> "nested": {"a": 1.0, "b": {"c": null}}
#> },
#> "values": {},
#> "bindings": {},
#> "meta": {}
#> }Reading
The reader uses jsonlite with simplifyVector = FALSE,
then converts each part of the result to an R value:
- Arrays containing only scalars or
nullbecome atomic vectors. The elements other thannulldetermine the type. Numeric arrays become integer vectors if every number was parsed as an integer, and double vectors otherwise. Mixing other types is an error. - Objects with a
$typekey are decoded using the rules above. Other objects become named lists. - Arrays containing an object or another array become unnamed lists.
An unknown $type raises an error that identifies the
value. Use unknown_types = "keep" to keep its raw
representation instead. Duplicate keys are always an error.
Writing and reading a supported value preserves it, subject to the
precision and attribute rules above. The following example checks this
with identical(). When you write a snapshot,
producer is updated to record the versions of shinysnap,
shiny, and R doing the writing.
x <- list(inputs = list(
sum = 0.1 + 0.2, tiny = 1e-300, dates = as.Date(c("2024-01-01", NA)),
M = matrix(c(1.5, NA, Inf, 2), 2), f = factor(c("a", NA), levels = c("a", "b"))
))
back <- snap_unserialize(snap_serialize(x))
identical(snap_inputs(back), x$inputs)
#> [1] TRUEThe bundle (.zip)
A bundle is a zip archive containing:
-
manifest.json: exactly the JSON above, with anattachmentssection. Each entry is a file record:{"$type": "file", "name": "data.csv", "size": 1234, "type": "text/csv", "path": "attachments/upload/data.csv"}; the fields are arrays when afileInput()held several files. -
attachments/<id>/<file name>: the uploaded files. Names use onlyA-Z a-z 0-9 . _ -. A counter distinguishes files whose names repeat. -
objects/<n>-<path>.rds: values written withunsupported = "rds".
Before extracting a bundle, shinysnap rejects entries with
.. path components or absolute paths. The total size of the
uncompressed files must be below
getOption("shinysnap.max_bundle_bytes", 100 * 1024^2).
Files are extracted into a new temporary directory. Paths in the
manifest must point to regular files inside its
attachments/ or objects/ directories.
Use snap_attachment(x, id) to get the extracted file
paths. Browsers do not allow code to set the value of a file input, so
uploaded files are kept in attachments, separate from
inputs. Your restore hooks can read them from there.
upload <- tempfile(fileext = ".csv")
writeLines(c("x,y", "1,2"), upload)
snap <- list(
inputs = list(n = 1L),
attachments = list(data = list(
name = "data.csv", size = file.size(upload), type = "text/csv", datapath = upload
))
)
bundle <- tempfile(fileext = ".zip")
snap_write(snap, bundle)
back <- snap_read(bundle)
readLines(snap_attachment(back, "data"))
#> [1] "x,y" "1,2"The .rds format
snap_write(format = "rds") stores the R object with
saveRDS(). These files cannot be read in a text editor and
may not work across versions. Reading serialized R objects can execute
code, so snap_read() requires trust = TRUE to
open them. Use this format only for files you trust.
