The serde_r feature provides direct serialization between Rust types and native R objects without going through an intermediate format like JSON. This enables efficient, type-preserving conversions that respect R’s native data structures.

πŸ”—Overview

Featureserde (JSON)serde_r (Native)
Intermediate formatJSON stringNone
Type preservationNo (all numbers β†’ f64)Yes (i32 stays i32)
NA handlingLimitedFull support via Option<T>
PerformanceExtra parse/stringifyDirect conversion
Smart Vec dispatchNoYes (Vec β†’ integer vector)

πŸ”—Enabling the Feature

# Cargo.toml
[dependencies]
miniextendr-api = { version = "0.1", features = ["serde_r"] }

# Or for both JSON and native R serialization:
miniextendr-api = { version = "0.1", features = ["serde_full"] }

πŸ”—Type Mappings

πŸ”—Serialization (Rust β†’ R)

Rust TypeR TypeNotes
boollogical(1)Scalar
i8/i16/i32integer(1)Widened to i32
i64/u64/f32/f64numeric(1)Converted to f64
String/&strcharacter(1)UTF-8 preserved
Option<T>::Some(v)TTransparent
Option<T>::NoneNULL
Vec<i32>integer vectorSmart dispatch
Vec<f64>numeric vectorSmart dispatch
Vec<bool>logical vectorSmart dispatch
Vec<String>character vectorSmart dispatch
Vec<struct>list of listsHeterogeneous
HashMap<String, T>named listKeys become names
BTreeMap<String, T>named listSorted keys
struct { fields }named listField names preserved
() / unit structNULL
unit enum variantcharacter(1)Variant name
newtype variantlist(Variant = value)Tagged
tuple variantlist(Variant = list(...))Tagged list
struct variantlist(Variant = list(a=..., b=...))Tagged named list

πŸ”—Deserialization (R β†’ Rust)

R TypeRust TypeNotes
logical(1)boolNA β†’ error or Option::None
integer(1)i32NA β†’ error or Option::None
numeric(1)f64NA β†’ error or Option::None
character(1)StringNA β†’ error or Option::None
integer vectorVec<i32>
numeric vectorVec<f64>
logical vectorVec<bool>
character vectorVec<String>
raw vectorVec<u8> / &[u8]
named liststruct / HashMapField matching
unnamed listVec<T> / tuplePositional
NULL() / Option::None

πŸ”—Basic Usage

πŸ”—Defining Serializable Types

use serde::{Serialize, Deserialize};
use miniextendr_api::{miniextendr, ExternalPtr};
use miniextendr_api::serde_r::{RSerializeNative, RDeserializeNative};

#[derive(Serialize, Deserialize, Clone, ExternalPtr)]
pub struct Point {
    pub x: f64,
    pub y: f64,
}

#[miniextendr]
impl Point {
    pub fn new(x: f64, y: f64) -> Self {
        Point { x, y }
    }
}

// Register the adapter traits
#[miniextendr]
impl RSerializeNative for Point {}

#[miniextendr]
impl RDeserializeNative for Point {}

// Registration is automatic via #[miniextendr].

πŸ”—Using from R

# Create a Point
p <- Point$new(1.0, 2.0)

# Serialize to R list
data <- p$to_r()
# list(x = 1.0, y = 2.0)

# Access fields
data$x  # 1.0
data$y  # 2.0

# Deserialize from R list
p2 <- Point$from_r(list(x = 3.0, y = 4.0))
p2$x  # 3.0
p2$y  # 4.0

# Round-trip
original <- Point$new(5.0, 6.0)
restored <- Point$from_r(original$to_r())
identical(original$x, restored$x)  # TRUE

πŸ”—Smart Vec Dispatch

One of the key features of serde_r is smart vector dispatch. When serializing Vec<T>, the serializer automatically chooses the most efficient R representation:

// Vec<i32> -> integer vector (atomic)
let ints = vec![1, 2, 3, 4, 5];
// Serializes to: c(1L, 2L, 3L, 4L, 5L)

// Vec<f64> -> numeric vector (atomic)
let floats = vec![1.1, 2.2, 3.3];
// Serializes to: c(1.1, 2.2, 3.3)

// Vec<String> -> character vector (atomic)
let strings = vec!["a".to_string(), "b".to_string()];
// Serializes to: c("a", "b")

// Vec<Point> -> list of lists (heterogeneous)
let points = vec![Point { x: 1.0, y: 2.0 }];
// Serializes to: list(list(x = 1.0, y = 2.0))

πŸ”—NA/NULL Handling

πŸ”—Option for NA Support

Use Option<T> to represent potentially missing values:

#[derive(Serialize, Deserialize, ExternalPtr)]
pub struct Record {
    pub id: i32,                    // Required
    pub name: Option<String>,       // Optional (can be NULL)
    pub value: Option<f64>,         // Optional (can be NULL)
}

From R:

# Create with all values
r1 <- Record$from_r(list(id = 1L, name = "test", value = 3.14))

# Create with missing values
r2 <- Record$from_r(list(id = 2L, name = NULL, value = NULL))

# Serialize back
r2$to_r()
# list(id = 2L, name = NULL, value = NULL)

πŸ”—Nested Structures

serde_r handles arbitrarily nested structures:

#[derive(Serialize, Deserialize)]
pub struct Level3 {
    pub data: Vec<f64>,
    pub flag: bool,
}

#[derive(Serialize, Deserialize)]
pub struct Level2 {
    pub level3: Level3,
    pub values: Vec<i32>,
}

#[derive(Serialize, Deserialize)]
pub struct Level1 {
    pub level2: Level2,
    pub name: String,
}

#[derive(Serialize, Deserialize, ExternalPtr)]
pub struct DeepNest {
    pub level1: Level1,
}

From R:

# Create from deeply nested R list
deep <- list(
  level1 = list(
    level2 = list(
      level3 = list(
        data = c(1.0, 2.0, 3.0),
        flag = TRUE
      ),
      values = c(10L, 20L, 30L)
    ),
    name = "nested"
  )
)

dn <- DeepNest$from_r(deep)

πŸ”—Enum Serialization

πŸ”—Unit Variants

Unit enum variants serialize to character strings:

#[derive(Serialize, Deserialize)]
pub enum Status {
    Active,
    Inactive,
    Pending,
}

From R:

# Unit variant -> character
status <- "Active"  # Deserializes to Status::Active

πŸ”—Data Variants

Data-carrying variants serialize to tagged lists:

#[derive(Serialize, Deserialize)]
pub enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
}

From R:

# Circle { radius: 5.0 } serializes to:
list(Circle = list(radius = 5.0))

# Rectangle { width: 10.0, height: 20.0 } serializes to:
list(Rectangle = list(width = 10.0, height = 20.0))

πŸ”—HashMap/BTreeMap

Maps with string keys become named R lists:

use std::collections::HashMap;

#[derive(Serialize, Deserialize, ExternalPtr)]
pub struct Config {
    pub settings: HashMap<String, i32>,
    pub metadata: HashMap<String, String>,
}

From R:

cfg <- Config$from_r(list(
  settings = list(timeout = 30L, retries = 3L),
  metadata = list(author = "test", version = "1.0")
))

data <- cfg$to_r()
data$settings$timeout  # 30L
data$metadata$author   # "test"

πŸ”—Standalone Functions

For one-off conversions without registering types:

use miniextendr_api::serde_r::{to_r, from_r};

#[miniextendr]
pub fn convert_to_r() -> SEXP {
    let data = vec![1, 2, 3, 4, 5];
    to_r(&data).expect("serialize")
}

#[miniextendr]
pub fn convert_from_r(sexp: SEXP) -> Vec<i32> {
    from_r(sexp).expect("deserialize")
}

πŸ”—Error Handling

Deserialization can fail for various reasons:

use miniextendr_api::serde_r::from_r;

#[miniextendr]
pub fn safe_deserialize(sexp: SEXP) -> Result<Point, String> {
    from_r::<Point>(sexp).map_err(|e| e.to_string())
}

Error types include:

  • TypeMismatch - Wrong R type for target Rust type
  • MissingField - Required struct field not in list
  • InvalidVariant - Unknown enum variant name
  • LengthMismatch - Wrong length for tuple/array
  • UnexpectedNa - NA where not allowed
  • Overflow - Numeric overflow in conversion

πŸ”—Integration with R Object Systems

πŸ”—With R6

library(R6)

MyClass <- R6Class("MyClass",
  public = list(
    x = NULL,
    y = NULL,
    initialize = function(x, y) {
      self$x <- x
      self$y <- y
    },
    to_list = function() list(x = self$x, y = self$y)
  )
)

obj <- MyClass$new(1.0, 2.0)
point <- Point$from_r(obj$to_list())

πŸ”—With S4

setClass("S4Point", slots = c(x = "numeric", y = "numeric"))
s4obj <- new("S4Point", x = 3.0, y = 4.0)

# Extract slots as list
point <- Point$from_r(list(x = s4obj@x, y = s4obj@y))

πŸ”—With S7

library(S7)

S7Point <- new_class("S7Point",
  properties = list(x = class_double, y = class_double)
)

s7obj <- S7Point(x = 5.0, y = 6.0)
point <- Point$from_r(list(x = prop(s7obj, "x"), y = prop(s7obj, "y")))

πŸ”—With Environments

e <- new.env()
e$x <- 7.0
e$y <- 8.0

point <- Point$from_r(as.list(e))

πŸ”—Comparison with IntoList Derive

miniextendr also provides #[derive(IntoList)] for simpler struct-to-list conversion. Here’s how they compare:

FeatureIntoListserde_r
Derive macroYesNeeds serde derives
DeserializationNo (one-way)Yes (bidirectional)
Enum supportNoYes
Smart Vec dispatchNoYes
HashMap/BTreeMapNoYes
Option/NANoYes
Nested structsYesYes

Use IntoList for simple one-way struct-to-list conversion. Use serde_r when you need full bidirectional serialization, enum support, or smart vector handling.