miniextendr exposes one owned [DataFrame] type and two conversion traits that mirror the scalar/vector surface (IntoR / TryFromSexp):

TraitMethodDirectionAnalogue
IntoDataFramerows.into_dataframe()? -> DataFrameRust → RIntoR
FromDataFrameVec::<Row>::from_dataframe(&df)? -> Vec<Row>R → RustTryFromSexp

Both verbs live on the data (Vec<Row> / &DataFrame), not on a companion type. Errors are a single [DataFrameError]. There is one NA contract: missing cells round-trip as Option<T> fields.

🔗The owned DataFrame type

DataFrame wraps a validated data.frame SEXP. It implements IntoR (returns the backing SEXP) and TryFromSexp (validates the data.frame class), so it flows through #[miniextendr] signatures like any other type:

use miniextendr_api::dataframe::{DataFrame, FromDataFrame, IntoDataFrame};
use miniextendr_api::{DataFrameRow, IntoList, miniextendr};

#[derive(Clone, IntoList, DataFrameRow)]
pub struct Point { pub x: f64, pub y: f64, pub label: String }

// Rust → R: build a data.frame from a row vector.
#[miniextendr]
fn make_points(df: DataFrame) -> DataFrame {
    let rows: Vec<Point> = Vec::<Point>::from_dataframe(&df).unwrap();   // R → Rust
    rows.into_dataframe().unwrap()                                       // Rust → R
}

🔗Parallel fast paths (feature = "rayon")

Explicit _par variants produce the same DataFrame / Vec<Row> as the sequential verbs — parallelism is an opt-in method, not a hidden threshold:

let df   = rows.into_dataframe_par()?;          // #777 flattened (column,row-range) fill
let rows = Vec::<Point>::from_dataframe_par(&df)?; // #765 off-main-thread row assembly

Dropping _par (building without the rayon feature) degrades cleanly to the sequential call — the verb name is stable across feature sets.

🔗Builder for heterogeneous columns (feature = "rayon")

When you are filling columns directly (not transposing a Vec<Row>), use the builder, which yields a DataFrame:

let df = DataFrame::builder(nrow)
    .column::<f64>("x", |chunk, off| { /* fill */ })
    .column_str("label", |i| Some(format!("p{i}")))
    .build();

🔗How #[derive(DataFrameRow)] wires this up

The orphan rule forbids the derive from writing impl IntoDataFrame for Vec<Row> in your crate (IntoDataFrame and Vec are both foreign there). Instead the derive implements the #[doc(hidden)] local marker DataFrameRowConvert on your local Row type, and miniextendr_api carries the blanket impl<T: DataFrameRowConvert> IntoDataFrame for Vec<T>. You still call the public verbs — the indirection is invisible.

FromDataFrame is emitted for every struct row shape: simple scalar fields, column expansion ([T; N], Vec<T> + width, Vec<T>/Box<[T]> + expand), struct-flatten (nested DataFrameRow fields, including several levels of nesting), and opaque list-columns (un-annotated Vec<scalar> / Box<[scalar]> fields stored as VECSXP list-columns — each row’s element is deserialized via Vec<elem>: TryFromSexp and .into()-converted to the field container type). Each reader is the exact inverse of its writer — it regroups the suffixed expansion columns, reads each <field>_-prefixed sub-frame back through the nested type’s own reader, and deserializes opaque list-column elements per row.

FromDataFrame is also emitted for tagged enum row shapes (enums with #[dataframe(tag = "...")]): scalar Single fields (any variant mix of payload + unit variants), column-expansion fields ([T; N] fixed-array and Vec<T> + width in variants), struct-flatten variant fields (inner DataFrameRow structs), nested payload-bearing enum flatten (inner enum that itself has a reader), as_factor unit-only nested enums, and map-column fields (HashMap/BTreeMap with bare-scalar keys and values — the <field>_keys / <field>_values list-columns are zipped back into the map per row). The reader reads the tag column first, then per-row dispatches to each active variant’s field assemblers. Struct-flatten/nested-enum paths densify the sub-frame (keeping only present rows) before recursing into the inner reader. BTreeMap round-trips byte-for-byte (sorted key order); HashMap preserves the key→value associations but not the (non-deterministic) column order.

A few shapes still have no reader and return a clear DataFrameError from from_dataframe (rather than failing to compile): borrowed fields (&[T] / &str — owned R data can’t produce a borrow), #[dataframe(skip)] fields (the column was never written), #[dataframe(as_list)] and opaque non-scalar collection columns (HashMap, HashSet, Vec<Option<T>> list-columns), #[dataframe(tag)] structs, #[dataframe(conflicts = "string")] enums, and tagless or skip-field enums.

Ragged width/expand columns round-trip losslessly because the writer only ever pads trailing slots with NA: the reader flattens the present values back into the Vec, and re-writing pads the same trailing slots again.

🔗serde rows

Types that derive serde::Serialize / Deserialize convert through the SerdeRows<T> newtype (so the serde path never collides with the derive’s concrete Vec<Row> conversions):

let df   = SerdeRows(rows).into_dataframe()?;
let rows = SerdeRows::<Row>::from_dataframe(&df)?.into_inner();

🔗Migration from the legacy surface

WasNow
DataFrameView, convert::DataFrame<T> (duplicate DataFrame types)one DataFrame
ToDataFrame<Companion> return wrapper + value.to_data_frame()rows.into_dataframe()?
convert::SerializeDataFrame<T> / AsSerializeRow<T>serde::SerdeRows(rows).into_dataframe()?
Row::try_from_dataframe(sexp) (bare String error)Vec::<Row>::from_dataframe(&df)? (DataFrameError)
RDataFrameBuilder::new(n)DataFrame::builder(n)
four conversion error typesone DataFrameError

The redundant public types above have been removed (#781) — there is no backwards-compat shim. The legacy companion methods (to_dataframe, from_rows, try_from_dataframe) remain as the internal engine the trait impls delegate to. The serde columnar assembler has been aligned with the façade (#783): the internal columnar newtype is now folded into DataFrame (all serde column helpers return DataFrame directly), and the streaming serde-row builder is now SerdeRowBuilder (paired with SerdeRows). Two builders remain distinct from DataFrame::builder: SerdeRowBuilder<T> (serde feature) for incremental serde rows assembled into one DataFrame, and NamedDataFrameListBuilder (core, in dataframe — no serde needed; also the output shape for group_by(...).frames()) for a named list of frames.