Conversion · Seurat object (RDS) → h5Seurat
How to Convert Seurat object to h5Seurat (Without Losing Your Metadata)
SaveH5Seurat doesn't corrupt your data on Seurat v5 objects, it just refuses to write until you rejoin split layers, and knowing why saves you an hour of guessing.
By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 3 min read
- Seurat object (RDS)
- .rds, .RDS · coordinates: n/a
- h5Seurat
- .h5Seurat · coordinates: n/a
You need this conversion when your Seurat object has to leave the R ecosystem: handing data to a collaborator who works in scanpy, archiving in a format that isn't tied to your exact Seurat package version, or as the required first hop toward an h5ad file via SeuratDisk's Convert(). h5Seurat itself is rarely the final destination; it's the HDF5-shaped waypoint between an RDS file and AnnData.
What changes in the conversion is structural, not numerical. Seurat's S4 slots become HDF5 groups: each assay's counts, data, and scale.data layers land as separate datasets, reductions and graphs get their own groups, and meta.data columns survive as long as they're base R types. Anything you tucked into @misc or @commands isn't guaranteed to come along, so don't assume it did.
The most common way this goes wrong isn't silent corruption, it's a hard stop. If you're on Seurat v5 and your object went through split() for integration, SaveH5Seurat() sees multiple counts/data layers per assay (counts.batch1, counts.batch2, ...) and has no slot to put them in, because the h5Seurat writer predates Assay5. You get an error, not a broken file, which is the safer failure mode. The real silent trap shows up later: LoadH5Seurat() with an explicit assays list that omits scale.data loads without complaint, and your PCA or heatmap code fails downstream with no clue the layer was never there.
The commands
Type your file names once; every command below updates.
01SeuratDisk::SaveH5Seuratv0.0.0.9021
rseu <- readRDS("sample.rds") seu[["RNA"]] <- JoinLayers(seu[["RNA"]]) SaveH5Seurat(seu, filename = "sample.h5Seurat", overwrite = TRUE)JoinLayers collapses Seurat v5's split counts.batch1/counts.batch2-style layers back into single counts and data layers, which is the structure SaveH5Seurat expects. Run this first on any object that has been through split()/integration before you try to save. Assumes the object has at least one non-empty counts or data slot in the RNA assay.
02SeuratDisk::SaveH5Seuratv0.0.0.9021
rseu[["RNA"]] <- as(seu[["RNA"]], "Assay") SaveH5Seurat(seu, filename = "sample.h5Seurat", overwrite = TRUE)If SaveH5Seurat still fails after JoinLayers, the object may still carry an Assay5 class that the mojaveazure SeuratDisk writer doesn't recognize. Coercing to the legacy Assay class downgrades the internal representation; you lose the ability to re-split layers later, so only do this on a copy you're saving for export, not your working object.
03srtdisk
rremotes::install_github("mianaz/srtdisk") library(srtdisk) SaveH5Seurat(seu, filename = "sample.h5Seurat", overwrite = TRUE)srtdisk is a fork of SeuratDisk with native Assay5 support (tested against Seurat v5.5.1.9999 and v5.6-beta), so you can skip the downgrade-to-Assay step entirely. Use this path if you need to keep the object as true Assay5 for other reasons, or if the JoinLayers + downgrade route errors with 'Call assays must have either a counts or data slot'.
04SeuratDisk::Convertv0.0.0.9021
rConvert("sample.h5Seurat", dest = "h5ad")h5Seurat is rarely the destination itself; this converts the .h5Seurat file you just wrote into an AnnData .h5ad file for use in scanpy or squidpy. It reads {output} from the SaveH5Seurat step, so run it only after that file exists and opens cleanly with LoadH5Seurat.
Coordinates, strand, names, builds
Neither format carries genomic coordinates or strand, so the usual 0-based/1-based and chr1-vs-1 questions don't apply here. What does change is how the object's internal structure maps onto disk. Seurat's S4 slots (@assays, @meta.data, @reductions, @graphs) become HDF5 groups in the .h5Seurat file; each assay's counts, data, and scale.data layers become separate datasets under that assay's group, which is exactly why Seurat v5's split-layer structure (counts.batch1, counts.batch2, ...) doesn't map cleanly until you JoinLayers first. meta.data columns round-trip fine for base R types (character, numeric, factor, logical); anything you stored as a list-column or a custom S4 object inside @misc or @commands is not guaranteed to survive and you should not rely on it without checking. Cell and feature names (colnames/rownames) are preserved as HDF5 dimnames, but graph objects (SNN/KNN graphs from FindNeighbors) and command logs can be dropped depending on the SeuratDisk version, so re-verify anything you depend on for downstream clustering after the round trip rather than assuming it came along for free.
Check the output before you trust it
01Cell and gene counts match the source object
rseu_check <- LoadH5Seurat("{output}") identical(dim(seu), dim(seu_check))Expected TRUE. If dimensions differ, layers were dropped or only partially loaded.
02Expected layers are present in the reloaded assay
rnames(seu_check[["RNA"]]@layers)Expected At minimum "counts" and "data"; "scale.data" too if the original object had it. A missing layer here means downstream ScaleData/RunPCA will silently need to be rerun.
03Metadata columns are identical before and after
rsetdiff(colnames(seu@meta.data), colnames(seu_check@meta.data))Expected character(0). Any names returned are metadata columns that didn't survive the round trip.
04Reductions (PCA/UMAP) carried over
rReductions(seu_check)Expected The same reduction names as Reductions(seu), e.g. "pca" and "umap". An empty result means you'll need to rerun dimensionality reduction.
05Cell barcodes are in the same order
ridentical(colnames(seu), colnames(seu_check))Expected TRUE. A FALSE here signals reordering that will misalign any external annotation you join back on barcode.
Errors you will see, and what they mean
- argument is of length zero
- Cause: SaveH5Seurat expects VariableFeatures(seu) to be populated; if you never ran FindVariableFeatures() (common on objects assembled straight from a count matrix, or subsetted objects that dropped the HVG slot), the writer hits an empty vector where it expects feature names. Fix: Run FindVariableFeatures(seu) before saving, even if you don't need the HVGs downstream, or check length(VariableFeatures(seu)) > 0 first.
- Call assays must have either a counts or data slot
- Cause: The RNA (or other) assay's counts and data slots are both empty, usually because JoinLayers wasn't run on a v5 object with split layers, or because the assay was rebuilt without recopying counts. Fix: Check slot(seu[["RNA"]], "counts") and slot(seu[["RNA"]], "data") aren't 0x0 matrices before saving; run JoinLayers(seu[["RNA"]]) if the object went through split()/integration.
- SaveH5Seurat throws on Seurat v5 objects with split layers (GitHub issue #147)
- Cause: The h5Seurat format has no slot for Seurat v5's multi-layer Assay5 structure (counts.batch1, counts.batch2, ...); the mojaveazure SeuratDisk package predates Assay5 and has no native support for it. Fix: JoinLayers before saving, or switch to the srtdisk fork which adds Assay5 support instead of forcing a downgrade.
- Downstream analysis silently missing scale.data after LoadH5Seurat
- Cause: LoadH5Seurat's assays argument defaults to loading only some layers per assay if you passed an explicit list without scale.data; nothing errors, the slot is just empty. Fix: When calling LoadH5Seurat("{output}", assays = list(...)), explicitly include "scale.data" in the vector for any assay you'll run PCA or heatmaps on, or omit the assays argument to load everything.
Questions people ask
- Why does SaveH5Seurat fail on my Seurat v5 object?
Seurat v5 stores expression data in layers, and workflows that use split() or integration create multiple counts/data layers per assay (counts.batch1, counts.batch2, and so on). The mojaveazure SeuratDisk package predates Assay5 and expects one counts layer and one data layer per assay, so it throws instead of guessing how to merge them. Run JoinLayers(seu[["RNA"]]) before SaveH5Seurat to collapse them back.
- Do I lose metadata converting Seurat RDS to h5Seurat?
Standard meta.data columns (character, numeric, factor, logical) survive the conversion because they map directly to HDF5 datasets. Anything stored in @misc, @commands, or as a custom object inside meta.data is not guaranteed to round-trip, so check those slots specifically rather than assuming everything came along.
- Should I convert straight to h5ad instead of going through h5Seurat?
There's no direct Seurat-RDS-to-h5ad path; SeuratDisk's Convert() function requires an intermediate .h5Seurat file first. Treat h5Seurat as a waypoint, not a destination: save to h5Seurat, verify it loads correctly, then Convert() to h5ad if that's what you actually need.
- What's the difference between SeuratDisk and srtdisk?
SeuratDisk (mojaveazure) is the original package but has no native support for Seurat v5's Assay5 class, which is why it fails on split layers. srtdisk is a fork that adds Assay5 support (tested against Seurat v5.5.1.9999 and v5.6-beta), so it's the better choice if you don't want to downgrade your assay class to save it.
- Is h5Seurat a stable format I can archive data in long term?
No. It's explicitly an intermediate format built to bridge Seurat and AnnData via SeuratDisk's Convert() function, not a maintained long-term storage standard. For archiving, keep the original RDS alongside any h5Seurat export you generate for interoperability.
Related pages
- Convert · How to Convert h5ad to Seurat object (Without Losing Your Metadata)
- Convert · How to Convert Loom to Seurat object (Without Losing Your Metadata)
- Convert · How to Convert Seurat object to h5ad (Without Losing Your Metadata)
- Convert · How to Convert Seurat object to SingleCellExperiment (Without Losing Your Metadata)
- Convert · How to Convert SingleCellExperiment to Seurat object (Without Losing Your Metadata)
- Glossary · Count matrix
- Glossary · Heatmap
Related reading on the blog
Sources
- Seurat v5 Essential Commands — JoinLayers and split-layer behavior in Seurat v5
- SaveH5Seurat: Save a Seurat object to an h5Seurat file (rdrr.io) — Basic SaveH5Seurat usage and overwrite argument
- Error in saveh5seurat after upgrading to Seurat v5 · Issue #147 · mojaveazure/seurat-disk — SaveH5Seurat failing on Seurat v5 split layers
- srtdisk: Seurat v5 compatible HDF5 converter for single-cell data — Fork with native Assay5 support as an alternative to downgrading
- Creating a H5Seurat object error · Issue #172 · mojaveazure/seurat-disk — argument is of length zero error tied to missing variable features
- Can't convert Seurat 5 object to h5ad AnnData · Issue #7196 · satijalab/seurat — Call assays must have either a counts or data slot error