Skip to main content
A trainset is Vec<E> where E is any struct you define: the row. Rows are the unit of trainsets and of metric ground truth, and they are signature-independent — a row can carry gold labels and metric-only fields the module never sees (HotpotQA supporting facts, difficulty tags, source ids). The connection between a row and a module is the ToInput<I> trait: the evaluation loop and optimizers project each row into the module’s input with to_input() and hand the full row to your metric. DataLoader is the ingestion path that produces Vec<E> from JSON, CSV, Parquet, and HuggingFace sources. There is no untyped row type in the public contract: custom mappers work with RowRecord at the load boundary.
Few-shot demos are a separate type: Demo<S> is the signature-bound input/output pair rendered into the prompt, and it lives with the predictor. See Predict for Demo<S>, .demo(...), and .with_demos(...).

Row structs

A row struct is plain data. It names no signature and marks no fields: which fields matter is decided at the call site, by name.
A field is used when the target type declares a field of that name; fields the target does not declare are ignored. supporting_facts above is metric-only for QA not because it is marked, but because QAInput and QAOutput have no such field. The same row type serves any signature whose input it can fill. The same row type flows through the whole loop: Because the metric sees the row rather than a signature-shaped pair, ground truth does not have to fit the module’s output type: a metric can score a QAOutput prediction against supporting facts the module never produced.

ToInput and ToOutput

ToInput projects a row into a module’s input type. ToOutput is its counterpart for gold output, used to seed labeled few-shot demos (Demo::new(row.to_input()?, row.to_output()?)); optimizers that harvest demos from traces do not need it. Both are fallible because the derived impl resolves fields by name at runtime; the tuple impls never fail. Three ways to get an impl: Tuple rows make zero-boilerplate inline trainsets:

#[derive(Example)]

The derive marks a struct as a trainset row. It takes no arguments and no field attributes, and generates blanket ToInput<I>/ToOutput<O> impls: the row projects into any target type by serializing itself and deserializing the target out of it, matching fields by name.
Requirements and behavior:
  • The row must be a struct with named fields and must implement serde::Serialize (derive it alongside Example).
  • Fields the target type does not declare are ignored, so one row type can serve several signatures and carry metric-only columns.
  • Resolution happens at runtime, not compile time. A field the target requires but the row lacks — or one whose type does not deserialize — returns an error naming both types, propagated as an evaluation or compile error. Use tuple rows where you want the check at compile time.
Because the impls are blanket, the target type comes from the call site: the module’s Input for to_input(), the signature’s Output for to_output(). Nothing on the row names a signature.
A row whose gold fields do not line up with the signature’s Output needs no special handling: nothing is generated per-signature, so to_input() works regardless and only a to_output() call that cannot be satisfied fails. Metrics read gold fields from the row directly, so rows labeling a subset of a multi-field output (a dataset with a gold answer against a signature that also outputs reasoning) simply never call to_output(). dspy_rs::core::example::project is the underlying helper (project::<T, U>(&T) -> Result<U>) if you need the same field-name projection outside a row impl.

DataLoader

DataLoader is a unit struct whose associated functions load JSON, CSV, Parquet, and HuggingFace sources. All loaders are generic over the row struct and return anyhow::Result<Vec<E>> where E: serde::de::DeserializeOwned + facet::Facet. The row’s Facet shape determines required fields and drives type-aware coercion; derive it alongside Deserialize:
A source field missing from a row is DataLoadError::MissingField unless the row field is Option<_>, in which case it deserializes as None. Each of load_json, load_csv, load_parquet, and load_hf has a _with mapper overload (load_json_with, load_csv_with, load_parquet_with, load_hf_with) that takes the same parameters plus a closure Fn(&RowRecord) -> anyhow::Result<E>, with no Deserialize or Facet bound on E. Mapper overloads bypass shape-driven conversion entirely: opts is accepted for API parity but is not applied, so field_map and unknown_fields have no effect on _with calls.

TypedLoadOptions

UnknownFieldPolicy variants: The shape-driven path also applies tolerant scalar coercion before strict serde deserialization, driven by the row’s Facet shape: a "4" cell fills a String field as "4" and an i64 field as 4; string cells convert to declared Int, Float, and Bool ("true"/"false", case-insensitive) fields when they parse cleanly. Values that do not convert pass through unchanged so serde surfaces a precise TypeMismatch.

RowRecord

RowRecord is the public raw-row type passed to _with mapper closures. RowRecord::get::<T>(key) deserializes a typed value from a field, where T: DeserializeOwned + 'static. It returns DataLoadError::MissingField when the key is absent and DataLoadError::TypeMismatch on deserialization failure. String reads coerce scalar JSON numbers and booleans into strings for ergonomic CSV mapping.
Mapper closure errors are wrapped as DataLoadError::Mapper with the failing row index.

DataLoadError

DataLoadError implements std::error::Error via thiserror. Public loaders return anyhow::Result, with DataLoadError as the wrapped source. The module also exposes is_url(path: &str) -> bool, the helper the loaders use to decide between filesystem and HTTP(S) fetching.

Versioning

Data loading is versioned under data::v1. The module tree is data::v1::dataloader and data::v1::utils; data/mod.rs re-exports v1::*, so unversioned paths (dspy_rs::data::DataLoader) and versioned paths (dspy_rs::data::v1::dataloader::DataLoader) both resolve to the same items. The crate root flattens further: dspy_rs::DataLoader is the conventional import. Unversioned paths always track the current version.

Migration note

The signature-bound pair Example<S> { input, output } was removed. Trainset rows are now plain structs that project into a signature’s types through ToInput/ToOutput (this page), and few-shot demos are Demo<S> on Predict. Loaders that took a signature (load_json::<Sig>) now take the row struct (load_json::<Row>); TypedLoadOptions::field_map keys are row-struct field names instead of signature field names. The pre-v1 raw loaders were also removed. Use the typed load_* / load_*_with APIs instead of:
  • load_json(path, input_keys, output_keys)
  • load_csv(path, delimiter, has_headers, input_keys, output_keys)
  • load_parquet(path, input_keys, output_keys)
  • load_hf(dataset_name, subset, split, input_keys, output_keys, verbose)
  • save_json(...) and save_csv(...)

See also