rtb-tui 0.6.4

Reusable TUI widgets: forms, wizards, prompts, tables. Part of the phpboyscout Rust toolkit.
Documentation
---
title: Render helpers
description: render_table and render_json — exact output shape, trailing newlines, empty input, failure modes.
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.

## `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
 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:

```text
 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:

```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 — 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](05-crate.md#known-documentation-defects).

## Using both from one row type

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

```rust
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.