You need this guide if you have R code written against synapser 2.x. 3.0 is not a drop-in upgrade: function names, argument names, and the shape of the objects you get back have all changed, so an existing script will stop at its first Synapse call.
The failures are at least loud rather than silent — you’ll see
could not find function "synTableQuery",
unused argument (parentId = "syn123"), or
AttributeError: 'File' object has no attribute 'properties'
— and the fixes are mechanical once you know the four patterns
below.
If you’re writing new code rather than porting old code, you don’t need this guide at all: start from Data Upload, Download, and Delete and Tables.
Staying on 2.x costs you more than the new API:
reticulate to 1.25–1.28 and rjson to ≤ 0.2.21,
which holds back every other project sharing your R library. 3.0 drops
both ceilings.pandas and numpy caps). 3.0 supports
Python 3.10–3.14 and sets up its Python dependencies through
reticulate::py_require(), with no virtualenv to
manage.Previous versions of synapser wrapped a hand-curated subset of the
Python client behind R-idiomatic function names and camelCase arguments.
3.0 upgrades the reticulate/rjson dependency and, to keep pace with the
Python client’s newer
synapseclient.models/synapseclient.operations
architecture, generates the R layer directly from those Python OOP
models instead of maintaining a separate hand-written wrapper. The
result is an API that mirrors the Python client field-for-field, so most
existing synapser scripts will need updates.
This guide covers the Project/Folder/File/Table workflow — the paths most scripts use. The same patterns apply to every other entity type (Dataset, Views, Evaluation, Team, Wiki, …); see the full reference for exact signatures.
Upgrading costs a round of script updates (see below), but it buys you real capability, not just a version bump:
reticulate,
rjson) are no longer held to old versions, so installing
synapser doesn’t force older packages on the rest of your R
library.synCopy()/synSyncFromSynapse()/synWalk(),
hierarchy-wide
synDeletePermissions()/synListAcl() with a
dry_run preview, and JSON Schema binding/validation
(synBindSchema(), synValidateSchema(), …) are
all new in 3.0 — see New in 3.0:
capabilities that didn’t exist before below. None of these required
someone to hand-port them from the Python client; they came along for
free with the switch to generating the R layer directly from
synapseclient.models.VirtualTable,
DatasetCollection,
Agent/AgentSession, and
Organization/JSONSchema have no 2.x
counterpart at all, and the view types (EntityView,
MaterializedView, SubmissionView) replace
2.x’s
EntityViewSchema/MaterializedViewSchema/SubmissionViewSchema
while picking up the same column, query, and ACL verbs as
Table.synapseclient can be surfaced in R without waiting on a
hand-written wrapper to catch up — which is how the gap above
accumulated in the first place.Constructor(...) |> synMethod(...) |> synStore()
shape, with snake_case arguments that match the Python client’s field
names exactly. If the R docs for a function are unclear, the Python reference describes
the same method, argument for argument — that cross-reference didn’t
reliably work in 2.x, where argument names and calling conventions were
hand-curated per function.In short, synapser 2.x emphasizes compatibility with an older hand-maintained API surface, while 3.0 trades one migration pass for long-term gains: fewer dependency constraints, broader Synapse coverage (new entities plus recursive and schema workflows), and an API that stays aligned with the actively evolving Python client instead of drifting behind it.
The rest of this guide covers what to change in existing scripts to get there.
synapser 3.0 requires:
reticulate 1.44.0 or later. Earlier versions, including
the 1.28 that 2.x pinned, fail with
could not find a Python environment for /.If you set up pinned dependencies for 2.x (such as rjson
and reticulate), remove them, and remove the
r-reticulate virtualenv and any
RETICULATE_PYTHON setting so synapser can use its managed
environment via uv. Then
restart R:
Run reticulate::py_config() to check which Python
synapser is using. See Installation for
details.
Every entity is now a real object you construct directly —
File(...), Folder(...),
Table(...), Project(...), … — and every
operation on it is a “verb” function (synStore(),
synGet(), synSetProperties(),
synAddColumn(), synDeleteRows(), …) that takes
the instance as its first argument. You construct the object first, then
thread it through a chain of these verbs with the native pipe,
tidyverse-style, instead of reassigning a variable at every step:
# 2.x -- synStore() was one Python-backed function shared by every entity
# type, with Python duck-typing on `obj` internally; each step reassigned a
# variable rather than chaining
file <- File(path, parentId = "syn123")
file <- synStore(file)
file <- synGet(file$properties$id, downloadFile = FALSE)
# 3.0 -- construct the instance, then pipe it through verbs
file <- File(path = path, parent_id = "syn123") |>
synStore() |>
synGet(file_options = FileOptions(download_file = FALSE))This is the change everything else in this guide follows from. Two kinds of function take an instance this way:
synStore(), synGet(), and
synDelete() are single factory functions that accept any
supported entity — or, for synGet() and
synDelete(), a Synapse ID string instead — and apply the
behavior appropriate to that entity’s type.synAddColumn(),
synGetAcl(), synCopy(),
synWalk(), synBindSchema(), …) is a generic
that dispatches on the class of the object you pipe in. The same verb
name therefore works uniformly across every entity type that supports
it, instead of each operation being its own hand-written function
reconciling the differences between entity types internally.$properties
and synSetPropertiesProperties and annotations are now plain fields on the object. Read
them directly, and update them with the convenience function synSetProperties(),
which sets any number of fields, including annotations, in
one call and returns the entity so you can pipe it into
synStore():
# 2.x
entity$properties$id
entity$properties$name
entity$annotations$assay
# 3.0
entity$id
entity$name
entity$annotations$assay
synGet(synapse_id) |>
synSetProperties(
name = "new name",
version_label = "2",
annotations = list(assay = "RNA-seq")
) |>
synStore()Setting annotations replaces all of the entity’s
existing annotations. To add to them instead, combine them first:
synSetProperties(annotations = append(entity$annotations, list(key = "value"))).
# 2.x
synGet(entity, downloadFile = FALSE)
synStore(obj, forceVersion = FALSE, versionLabel = "2")
File(path, parentId = project$properties$id)
# 3.0
synGet(entity, file_options = FileOptions(download_file = FALSE))Some arguments moved rather than just changed case. Options that 2.x
passed to synStore() — forceVersion,
versionLabel — are fields on the entity itself in 3.0
(force_version, version_label), set at
construction or with synSetProperties().
synStore() itself takes only the entity, an optional
parent, and the per-type option objects
(StoreFileOptions(), StoreContainerOptions(),
StoreTableOptions(), …); there is no
force_version argument on it.
synSetProperties(), then an
explicit synStore()Directly assigning into $properties no longer works.
Properties are set with synSetProperties()
— which validates the field names against the object’s dataclass fields
— and piped into synStore():
# 2.x
entity$properties$name <- "new name"
entity <- synStore(entity)
# 3.0
entity <- synGet(synapse_id) |>
synSetProperties(name = "new name") |>
synStore()Table columns are the one exception — those are still mutated in
place on the $columns list before
synStore():
table$columns[["Expression"]]$name <- "Expression2"
table$columns[["Expression"]]$column_type <- "INTEGER"
table |> synStore()The column verbs synAddColumn(),
synDeleteColumn(), and synReorderColumn() work
the same way: they change the table object in place and return nothing,
so they are separate statements rather than links in a pipe into
synStore():
| Task | 2.x (legacy) | 3.0 (new) |
|---|---|---|
| Create + store an entity | File(path, parentId = "syn123") then
synStore(obj) |
File(path = path, parent_id = "syn123") \|> synStore() |
| Suppress a new version | synStore(obj, forceVersion = FALSE) |
File(path = path, parent_id = "syn123", force_version = FALSE) \|> synStore() |
| Set name / version label | entity$properties$name <- "x"; synStore(entity) |
synGet(id) \|> synSetProperties(name = "x") \|> synStore() |
| Move an entity | synMove(entity, newParent) |
synGet(id) \|> synSetProperties(parent_id = "syn456") \|> synStore() |
| Track provenance | synStore(obj, used = "syn123", executed = "syn456") |
synGet(id) \|> synSetProperties(activity = Activity(used = list(UsedEntity(target_id = "syn123")), executed = list(UsedEntity(target_id = "syn456")))) \|> synStore() |
| Remove provenance | synDeleteProvenance(entity) |
synDisassociateActivityFromEntity(Activity, parent = entity) |
| Get an entity | synGet(entity, ...) |
synGet(synapse_id, file_options = FileOptions(...)) |
| Get a specific version | synGet(entity, version = 1) |
synGet(synapse_id, version_number = 1L) |
| Skip downloading file content | synGet(entity, downloadFile = FALSE) |
synGet(synapse_id, file_options = FileOptions(download_file = FALSE)) |
| Delete a version only | synDelete(obj, version = 1) |
synDelete(entity, version = 1L, version_only = TRUE) |
| Query a table | synTableQuery(sql) |
synQuery(sql) |
| Create a table column | synCreateColumn(name = "x", columnType = "STRING") |
Column(name = "x", column_type = "STRING") |
| Add a table column | manual list manipulation | table \|> synAddColumn(Column(name = "x", column_type = "STRING")),
then table \|> synStore() |
| Remove a table column | manual list manipulation | table \|> synDeleteColumn(name = "x"), then
table \|> synStore() |
| Insert-or-update table rows | query, merge, re-store | table \|> synUpsertRows(values = df, primary_keys = list("Name")) |
| Build a table from a CSV, inferring columns | synBuildTable(name, parent, "genes.csv") |
Table(name = ..., parent_id = ...) \|> synStore() \|> synStoreRows(values = "genes.csv", schema_storage_strategy = "INFER_FROM_DATA") |
Because 3.0 generates the R layer from the Python models, a 2.x function survives only if the models expose an equivalent. These are the removals most scripts will hit.
synBuildTable() →
Table(...) |> synStore() |> synStoreRows(values = ..., schema_storage_strategy = "INFER_FROM_DATA").
The table has to exist before rows can be stored into it, so the
synStore() step is not optional.synTableQuery() →
synQuery()synMove() →
synSetProperties(parent_id = ...) |> synStore()synGetChildren() →
synSyncFromSynapse(recursive = FALSE, download_file = FALSE)
and then the container’s $files/$folders, or
synWalk() to traverse a whole treesynGetAnnotations() /
synSetAnnotations() → the annotations
field: read entity$annotations, write
synSetProperties(annotations = list(...)) |> synStore()synGetProvenance() /
synSetProvenance() / synDeleteProvenance() /
synUpdateActivity() → the activity
field (an Activity() object) set via
synSetProperties(), and
synDisassociateActivityFromEntity(Activity, parent = entity)
to remove itsynCreateColumn() /
synCreateColumns() / synGetColumn() /
synGetColumns() /
synGetTableColumns() → the Column()
constructor and the table’s own $columns listsynCreateSnapshotVersion() →
synSnapshot()synRestGET() / synRestPOST() /
synRestPUT() / synRestDELETE(), and
the 2.x synRestGetAsync() family →
synRestGet(), synRestPost(),
synRestPut(), synRestDelete()synCreateTeam() →
Team(name = "...") |> synCreate();
synGetTeam() →
Team |> synGetFromId(id = id) or
Team |> synGetFromName(name = name);
synDeleteTeam() →
synDelete(team)synSendMembershipInvitation() →
synInviteToTeam();
synGetTeamOpenInvitations() →
synGetOpenInvitations();
synGetMembershipStatus() →
synGetUserMembershipStatus()synGetUserProfile() →
UserProfile |> synGetFromId(user_id = user_id) or
UserProfile |> synGetFromUsername(username = username)synGetWiki() →
WikiPage(owner_id = ..., id = ...) |> synGet(); storing
a wiki page →
WikiPage(owner_id = ..., title = ..., markdown = ...) |> synStore();
synGetWikiHeaders() →
as.list(WikiHeader() |> synGet(owner_id = ...));
synGetWikiAttachments() →
synGetAttachmentHandles(), or
synGetAttachment() to fetch one attachment. See Wikis.synGetEvaluation() →
Evaluation(id = ...) |> synGet();
synGetEvaluationByName() →
Evaluation(name = ...) |> synGet();
synGetEvaluationByContentSource() →
synGetEvaluationsByProject(project_id = ...). See Evaluation.synSubmit() →
Submission(entity_id = ..., evaluation_id = ...) |> synStore();
synGetSubmission() →
Submission(id = ...) |> synGet();
synGetSubmissionStatus() →
SubmissionStatus(id = ...) |> synGet(), then
synStore() to update it;
synGetSubmissions() →
Submission() |> synGetEvaluationSubmissions(evaluation_id = ...);
synGetSubmissionBundles() →
SubmissionBundle() |> synGetEvaluationSubmissionBundles(evaluation_id = ...).
See Submission.synCreateS3StorageLocation() /
synCreateStorageLocationSetting() →
StorageLocation(...) |> synStore();
synGetMyStorageLocationSetting() →
StorageLocation(storage_location_id = ...) |> synGet();
synSetStorageLocation(),
synGetProjectSetting(), and
synGetStsStorageToken() keep their names
but are now verbs piped from a Folder or
Project
(folder |> synSetStorageLocation(storage_location_id = ...)).synGetDownloadList() →
synDownloadListFiles();
synGetDownloadListManifest() →
synDownloadListManifest();
synRemoveFromDownloadList() →
synDownloadListRemove();
synClearDownloadList() →
synDownloadListClear(). synDownloadListAdd()
is new.Note the calling convention in the Team and UserProfile entries.
synGetFromId(), synGetFromName(),
synGetFromUsername(), and
synDisassociateActivityFromEntity() wrap Python
classmethods, so their first argument is the constructor
(Team, UserProfile, Activity) —
that is what selects the implementation — rather than an instance of it.
An empty instance works as the marker too
(Team() |> synGetFromId(id = id)), which is the style
the reference pages use.
These 2.x functions have no model-layer equivalent yet. Until they
get one, call the REST API
directly with
synRestGet()/synRestPost()/synRestPut()/synRestDelete(),
or keep that step of the pipeline in the Python client:
synCreateExternalS3FileHandle(),
synUploadFileHandle().synDownloadTableColumns(),
synGetConfigFile(),
synInvalidateAPIKey().The OOP-model generation doesn’t just rename old functions — it also
surfaces Python client functionality that synapser never exposed at all.
These functions have no 2.x equivalent, so there’s nothing to migrate
from, but existing scripts that worked around their absence
(manual recursion to bulk update ACLs, hand-rolled directory walks,
shelling out to synapseutils from Python) can now be
simplified.
synDeletePermissions() and synListAcl() are
new. Both work over a single entity or an entire container hierarchy at
once, so an audit or cleanup that used to mean writing your own
recursive walk over 2.x’s synGetChildren() — and getting
the edge cases (nested folders, inherited vs. local ACLs) right yourself
— is now one call. dry_run lets you preview exactly what a
recursive permissions change will touch before anything is deleted,
which matters once you’re operating on an entire project tree instead of
a single entity:
# Preview, then perform, a recursive ACL cleardown of a folder and its children
Folder(id = "syn123") |> synDeletePermissions(recursive = TRUE, include_container_content = TRUE, dry_run = TRUE)
Folder(id = "syn123") |> synDeletePermissions(recursive = TRUE, include_container_content = TRUE)
# List every ACL in a hierarchy, optionally as an ASCII tree
Folder(id = "syn123") |> synListAcl(recursive = TRUE, log_tree = TRUE)One semantic change on an existing function:
synGetPermissions(instance) now always returns your
own effective permissions — the old principalId
argument is gone. To check another user or team’s permissions, use
synGetAcl(instance, principal_id = ...) instead, which
still supports it:
synapser never exposed synapseutils’ copy,
syncFromSynapse, or walk utilities — getting
this functionality previously meant dropping out of R into a Python
script, or reimplementing the recursion (and its failure handling) by
hand with repeated synGetChildren()/synGet()
calls. In 3.0 they’re methods on any container object, so a bulk copy, a
full local mirror of a project, or a metadata-only inventory of an
entity tree is a single pipeline that stays in R:
# Recursively copy a folder, and everything in it, to a new parent
Folder(id = "syn123") |> synCopy(parent_id = "syn456")
# Recursively download a folder tree, populating $files/$folders/$tables/...
folder <- Folder(id = "syn123") |> synSyncFromSynapse(path = "~/local-copy")
# Traverse a hierarchy without downloading anything
for (entry in as.list(folder |> synWalk())) {
dirpath <- entry[[1]]; dirs <- entry[[2]]; nondirs <- entry[[3]]
cat(dirpath[[1]], "\n")
}synWalk() returns a GeneratorWrapper — the
same lazy-iterator object 2.x returned from
synGetChildren(), which no longer exists. Call
as.list() on it to materialize every result into a plain
list, or nextElem() to pull results one at a time; you
can’t loop over it directly the way you can a list. For the narrower job
synGetChildren() used to do — listing one container’s
immediate children without downloading them — use
synSyncFromSynapse(recursive = FALSE, download_file = FALSE)
and read the resulting $files/$folders.
Also new: entities can be bound to a JSON Schema and validated against it directly from R. This lets you enforce metadata/annotation standards on Files, Folders, and Tables as part of an R pipeline — catching entities with missing or malformed annotations programmatically instead of relying on manual review or the web UI — and derive annotation values automatically from the schema rather than entering them by hand:
folder <- Folder(id = "syn123")
folder |> synBindSchema(json_schema_uri = "myOrg-mySchema-0.0.1")
folder |> synValidateSchema()
folder |> synUnbindSchema()synBindSchema() returns the binding record rather than
the entity, so bind the schema as its own statement and keep piping the
entity itself.
This applies to Files, Folders, Tables, Projects, and other
container/entity types. Creating the organization and schema themselves
uses the new Organization/JSONSchema
constructors — see ?Folder_synBindSchema for a full
example. The rest of the family: synGetSchema() and
synGetSchemaDerivedKeys() on any bindable entity, plus
synGetSchemaValidationStatistics() and
synGetInvalidValidation() on containers (Folder,
Project).
R’s bare numeric literals (1) are doubles. Parameters
the Synapse API expects as integers — version_number,
version, row IDs/versions in
synDeleteRows(df = ...) — must use the L
suffix (1L). Passing 1 instead of
1L can produce unexpected behavior or an API error. This
isn’t new in 3.0, but it surfaces more often now that raw integer
parameters are exposed directly instead of being hidden behind wrapper
defaults.
# ---- 2.x ----
library(synapser)
synLogin()
project <- Project("My Project")
project <- synStore(project)
file <- File(path = "data.csv", parentId = project$properties$id)
file <- synStore(file)
file <- synGet(file$properties$id, downloadFile = FALSE)
file$annotations <- c(assay = "RNA-seq")
file <- synStore(file, forceVersion = FALSE)
synDelete(project)# ---- 3.0 ----
library(synapser)
synLogin()
project <- Project(name = "My Project") |> synStore()
file <- File(path = "data.csv", parent_id = project$id) |> synStore()
file <- synGet(file$id, file_options = FileOptions(download_file = FALSE)) |>
synSetProperties(annotations = list(assay = "RNA-seq"), force_version = FALSE) |>
synStore()
synDelete(project)Task guides already updated for 3.0:
synAddColumn(), synDeleteColumn(),
synUpsertRows(), schema inference, dates) in detail.Setup and environment (unaffected by this migration — the API changes above don’t touch how you install or authenticate):
Reference: