Conversion · SingleCellExperiment (RDS) → Seurat object (RDS)
How to Convert SingleCellExperiment to Seurat object (Without Losing Your Metadata)
as.Seurat() copies your matrices and cell metadata fine; it quietly leaves your gene annotations and altExps behind.
By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 2 min read
- SingleCellExperiment (RDS)
- .rds · coordinates: n/a
- Seurat object (RDS)
- .rds, .RDS · coordinates: n/a
You need this conversion whenever your preprocessing lives in Bioconductor (scran, scater, DropletUtils) but your clustering, integration, or visualization work happens in Seurat, or when you download a public dataset that was released as a SingleCellExperiment and your lab's downstream pipeline expects a Seurat object. It's a one-way trip in practice: nobody round-trips this conversion repeatedly, so getting it right the first time matters.
as.Seurat() moves the counts and logcounts assays into Seurat's counts/data layers and copies colData into @meta.data. What it does not do is copy rowData (your gene symbols, biotypes, chromosome annotations) or altExps (spike-ins, CITE-seq ADT tags). Both of these are documented gaps, not edge cases you'll only hit occasionally, if your SCE has either, they vanish on conversion.
The most common way this goes wrong silently: the conversion "succeeds," dimensions look right, clustering runs fine, and three steps later a feature-level lookup (biotype filter, symbol mapping, antibody panel) returns empty instead of throwing an error. Nothing crashes. You just find out later that your annotation columns were never there.
The commands
Type your file names once; every command below updates.
01Seuratv5.5.1
rsce <- readRDS("sample.rds") seurat_obj <- as.Seurat(sce, counts = "counts", data = "logcounts") saveRDS(seurat_obj, "sample.rds")as.Seurat() maps the SCE's 'counts' assay to the counts layer and 'logcounts' to the data layer, and copies colData into @meta.data. It assumes your assays are literally named 'counts' and 'logcounts' (the OSCA convention), if they're named differently, pass the real names or rename the assays first. It does not touch rowData or altExps.
02SingleCellExperiment
rlibrary(Seurat); library(SingleCellExperiment) sce <- readRDS("sample.rds") seurat_obj <- CreateSeuratObject(counts = counts(sce), meta.data = as.data.frame(colData(sce))) saveRDS(seurat_obj, "sample.rds")Builds the Seurat object directly from the raw counts matrix and colData instead of going through as.Seurat(). Use this when as.Seurat() throws layer errors on v5-flavored objects or you want explicit control over meta.data column types. You lose logcounts unless you renormalize afterward with NormalizeData().
03Seurat
rseurat_obj <- readRDS("sample.rds") seurat_obj[["RNA"]][[]] <- as.data.frame(rowData(sce)) saveRDS(seurat_obj, "sample.rds")Neither as.Seurat() nor CreateSeuratObject() copies rowData, so gene-level annotations (symbol, biotype, chromosome) have to be reattached by hand to the assay's feature metadata. Assumes rowData(sce) rows are in the same gene order as the assay you already converted, check with identical(rownames(sce), rownames(seurat_obj)).
04Seurat
rseurat_obj <- readRDS("sample.rds") seurat_obj <- JoinLayers(seurat_obj) saveRDS(seurat_obj, "sample.rds")Rejoins split v5 layers (counts.1, counts.2, ... per batch) back into single counts/data layers. Run this before differential expression or any function that expects one layer per slot; assumes the layers came from the same original feature set and were only split for integration, not created from mismatched objects.
Coordinates, strand, names, builds
Neither object stores genomic coordinates, so there's no 0-based/1-based or chr-naming issue here. What actually changes is the object model and what silently rides along with the conversion. SingleCellExperiment uses named S4 slots (assays, colData, rowData, reducedDims, altExps) accessed with accessor functions; Seurat wraps assay data in layers (counts, data, scale.data in v5) accessed with [[, @meta.data, or GetAssayData(). colData transfers to @meta.data and reducedDims generally carries over as Reductions, but rowData does not transfer and must be reattached manually. altExps (spike-ins, CITE-seq ADT tags) are dropped entirely and have to be rebuilt as separate Seurat assays with CreateAssayObject(). Cell and gene ordering must stay in sync between the counts matrix and the metadata you attach, SCE enforces this automatically on subsetting, Seurat's CreateSeuratObject() does not, so a mismatched row order fails silently instead of erroring. Finally, layer structure is version-dependent: an SCE built to feed a Seurat v3-style assay can land in the wrong layer shape once opened in Seurat v5, which is the root of most "layer-mapping" pitfalls.
Check the output before you trust it
01Cell and gene counts match the source object
rdim(sce); dim(seurat_obj)Expected Same number of genes (rows) and cells (columns) in both objects, in the same order.
02Cell barcodes are identical and in the same order
ridentical(colnames(sce), colnames(seurat_obj))Expected TRUE. If FALSE, metadata and expression values are misaligned even though dimensions matched.
03colData columns landed in meta.data
rsetdiff(colnames(colData(sce)), colnames(seurat_obj@meta.data))Expected character(0), every colData column should show up as a meta.data column.
04counts layer actually has non-zero values
rsum(GetAssayData(seurat_obj, layer = "counts")[1:20, 1:20])Expected A positive number matching the equivalent slice of counts(sce); zero everywhere means the wrong assay name was passed to as.Seurat().
05rowData was actually reattached (it isn't by default)
rncol(seurat_obj[["RNA"]][[]])Expected 0 right after as.Seurat()/CreateSeuratObject(); should match ncol(rowData(sce)) only after you run the manual reattachment step.
06Dimensionality reductions carried over
rReductions(seurat_obj); reducedDimNames(sce)Expected Every reducedDim name in the SCE (e.g. PCA, UMAP) appears as a Reduction in the Seurat object.
Errors you will see, and what they mean
- seurat_obj[["RNA"]][[]] is an empty data frame after conversion
- Cause: as.Seurat() does not transfer rowData(sce), this is a documented limitation, not a bug in your code. Fix: Manually copy it: seurat_obj[["RNA"]][[]] <- as.data.frame(rowData(sce)), after confirming gene order matches with identical(rownames(sce), rownames(seurat_obj)).
- CITE-seq antibody/ADT counts or spike-ins are missing from the Seurat object
- Cause: altExps in the SingleCellExperiment (alternative experiments like ADT tags) are not carried over by as.Seurat() at all. Fix: Pull each altExp out and add it as its own assay: seurat_obj[["ADT"]] <- CreateAssayObject(counts = counts(altExp(sce, "ADT"))).
- as.Seurat() errors or produces mismatched dimensions between counts and data layers
- Cause: The SCE was built against Seurat v5-style layer expectations, but as.Seurat()'s layer mapping doesn't cleanly handle every v5 layer combination (counts, data, scale.data). Fix: Fall back to the manual CreateSeuratObject(counts = counts(sce), ...) path, then add normalized values separately with NormalizeData() or SetAssayData().
- as.Seurat() fails when the SCE only has a logcounts assay, no raw counts
- Cause: as.Seurat() expects a counts assay by default and errors or produces an empty counts layer when one doesn't exist. Fix: Call as.Seurat(sce, counts = NULL, data = "logcounts") to skip the counts layer explicitly.
- Downstream DE or FindMarkers behaves oddly after integration
- Cause: Seurat v5 split the assay into per-batch layers during integration and they were never rejoined into single counts/data layers. Fix: Run JoinLayers(seurat_obj) before differential expression or any function expecting one layer per slot.
Questions people ask
- Does as.Seurat() preserve rowData from my SingleCellExperiment?
No. This is a documented limitation, not a bug you introduced. You have to reattach rowData yourself after conversion with seurat_obj[["RNA"]][[]] <- as.data.frame(rowData(sce)), after confirming the gene order still matches.
- What happens to CITE-seq antibody counts (altExps) when I convert to Seurat?
They're dropped. as.Seurat() only converts the main experiment, not altExps like ADT or spike-in data. Pull each altExp out with altExp(sce, "ADT") and add it as its own Seurat assay with CreateAssayObject().
- Should I use as.Seurat() or CreateSeuratObject() to do this conversion?
Start with as.Seurat(), it's less code and handles the standard counts/logcounts mapping. Switch to CreateSeuratObject(counts = counts(sce), meta.data = as.data.frame(colData(sce))) if as.Seurat() errors on layer dimensions, which tends to happen with Seurat v5-style objects.
- Why does my Seurat object have mismatched or empty layers after conversion?
Seurat v5 organizes assay data into counts/data/scale.data layers, and the mapping between an SCE's assays and those layers isn't always clean, especially if the SCE was built with v5 layer conventions in mind. If as.Seurat() produces mismatched dimensions, fall back to the manual CreateSeuratObject() path.
- My SingleCellExperiment only has logcounts, no raw counts, how do I convert it?
Pass counts = NULL explicitly: as.Seurat(sce, counts = NULL, data = "logcounts"). Without this, as.Seurat() expects a counts assay by default and will misbehave when it doesn't find one.
Related pages
- Convert · How to Convert Seurat object to SingleCellExperiment (Without Losing Your Metadata)
- Convert · How to Convert 10x HDF5 to Seurat object (and Why IDs Go Missing)
- Convert · How to Convert 10x MTX to Seurat object (and Why IDs Go Missing)
- Convert · How to Convert h5ad to Seurat object (Without Losing Your Metadata)
- Convert · How to Convert Seurat object to h5ad (Without Losing Your Metadata)
Related reading on the blog
Sources
- as.Seurat, Convert objects to Seurat objects
- Chapter 4: The SingleCellExperiment class, Introduction to Single-Cell Analysis with Bioconductor
- Transfer feature-level metadata from SingleCellExperiment in as.Seurat.SingleCellExperiment function, Seurat GitHub Issue #4205
- Seurat v5 Essential Commands
- Integrative analysis in Seurat v5
- Error converting SingleCellExperiment object to Seurat object using as.Seurat, GitHub Issue #4556