Skip to content

Render helpers

Two functions that turn a slice of rows into a string. They exist as a pair so one --output text|json flag can pick between them without the calling command formatting anything itself.

#[must_use]
pub fn render_table<R: Tabled>(rows: &[R]) -> String;

pub fn render_json<R: Serialize>(rows: &[R]) -> Result<String, RenderError>;

Both take a slice, so both accept &vec and &[..]. Neither prints anything — they return the string and leave the writing to you.

render_table output shape

The style is tabled's Style::psql(), applied unconditionally. There is no parameter, no builder and no configuration key that changes it — see Why the table style is fixed.

For two rows of struct Row { name: &'static str, count: u32 }:

 name  | count 
-------+-------
 alpha | 1     
 beta  | 2     

Columns are padded to the widest cell, so the rule width follows the data.

Column headers come from the field names of the Tabled derive, in declaration order. Rename them with #[tabled(rename = "...")] on the field; that is a tabled attribute, not an rtb-tui one.

The returned string always ends with a newline. If tabled did not produce one, render_table appends it, so print! is the right macro and println! will add a blank line.

render_table is infallible — there is no error type and no panic path for a type that implements Tabled.

What render_table does with an empty slice

An empty slice still renders the header and the separator rule, because the headers come from the type rather than from the data:

 name | count 
------+-------

It does not return an empty string, and it does not return a "no rows" message. If you want one, check rows.is_empty() before calling.

render_json output shape

serde_json::to_string_pretty, which is a two-space indent, plus a trailing newline appended by render_json. The top level is always a JSON array, including for one row and for zero rows:

[
  {
    "name": "alpha",
    "count": 1
  }
]

An empty slice renders as [] followed by a newline. Field names come from serde, so #[serde(rename = "...")] and #[serde(skip)] apply in the usual way — and note that serde and tabled have separate rename attributes, so a field renamed for one is not renamed for the other.

When render_json returns an error

render_json returns RenderError::Json(String) when serde_json::to_string_pretty fails on the rows. The payload is the underlying serde_json::Error already converted to a string.

In practice serde_json fails on very little. Measured against this crate at v0.6.3:

Input Result
HashMap<i32, _> — integer map keys Ok — keys are stringified, {"7": ...}
f64::NAN, f64::INFINITY Ok — both serialise as null
HashMap<(u8, u8), _> — tuple map keys Err(RenderError::Json("key must be a string"))
A hand-written Serialize impl that returns an error Err(RenderError::Json(_)) with that impl's message

So the realistic triggers are a map keyed by something that is not string-like, and a hand-written Serialize impl that refuses a value. Both are programmer mistakes in the row type rather than anything a user typed, which is why the error carries a string rather than the original serde_json::Error.

The rustdoc on render_json names non-finite floats and non-string map keys as the typical causes. Half of that is wrong — see Known documentation defects.

Using both from one row type

A type that feeds both helpers needs both derives, and they are independent:

use serde::Serialize;
use tabled::Tabled;

#[derive(Tabled, Serialize)]
struct Row {
    name: &'static str,
    count: u32,
}

Tabled renders each field through Display unless you tell it otherwise, which rules out nesting: a field whose type is another struct will not derive Tabled without a per-field tabled attribute, even though Serialize handles it happily. A nested shape is the one case where the text and JSON views of the same type genuinely diverge — the table flattens or drops what the JSON keeps.