---
title: Render helpers
description: Exact output shape, trailing newlines, empty-input behaviour and failure modes of render_table and render_json.
tags: [reference, output]
---
# 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.
```rust
#[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.
## Which `Tabled` to derive
`rtb-tui` re-exports its `tabled` dependency as `rtb_tui::tabled`.
Derive through it:
```rust
use rtb_tui::tabled::Tabled;
#[derive(Tabled)]
#[tabled(crate = "rtb_tui::tabled")]
struct Row {
name: &'static str,
}
```
The `crate` attribute matters. Without it the derive expands to
`::tabled::Tabled`, which only resolves if you also depend on `tabled`
directly, and then it resolves to *your* `tabled`. When that is a
different version from the one `rtb-tui` uses, it is a different trait,
and `render_table` rejects the row type with E0277 ("the trait bound
`R: tabled::tabled::Tabled` is not satisfied", with a note that there
are multiple versions of `tabled` in the graph).
rtb-tui 0.6.4 moved from `tabled` 0.21 to 0.22, which is how rtb-cli
0.9.1 hit exactly that error.
## `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](../explanation/03-why-the-output-helpers-are-a-pair.md#why-the-table-style-is-not-configurable).
For two rows of `struct Row { name: &'static str, count: u32 }`:
```text
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:
```text
```
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:
```json
[
{
"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. 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 0.6.3, and the first three rows re-checked at 0.6.4 with
`serde_json` 1.0.151:
| `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`.
## Using both from one row type
A type that feeds both helpers needs both derives, and they are
independent:
```rust
use rtb_tui::tabled::Tabled;
use serde::Serialize;
#[derive(Tabled, Serialize)]
#[tabled(crate = "rtb_tui::tabled")]
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.