Skip to content

Feature request: Macros / formatting hooks to control TOML serialization layout in toml_edit #1027

@ethever

Description

@ethever

Summary

toml_edit focuses on lossless editing, but when serializing Rust types into a DocumentMut (or string), developers often need fine-grained control over the resulting layout—e.g., inline vs. multiline tables, array breaking rules, key ordering, comment injection, and whitespace. Today this requires manual post-processing of the produced DocumentMut, which is brittle and verbose.

I’m proposing first-class formatting controls—either via attribute/derive macros or via a pluggable formatter API—so users can declaratively specify layout while still leveraging serde + toml_edit’s lossless model.

Motivation & use cases

Keep certain small structs as inline tables (e.g., { x = 1, y = 2 }) while keeping others expanded.

  • Force arrays to be inline or multiline (and wrap after N items).

  • Preserve or inject comments at field, table, or document level.

  • Control key ordering/grouping (e.g., “provider.* first, then strategy.*”).

  • Normalize spacing/blank lines between logical sections.

  • Enforce project-wide style without hand-editing the DocumentMut afterwards.

  • These are common needs in configuration tooling and code generators.

Current behavior

Given (simplified):

#[derive(serde::Serialize)]
struct ProviderConfig {
    read_only_rpc_urls: Vec<Vec<String>>,
    write_only_rpc_url: String,
}

#[derive(serde::Serialize)]
struct AppConfig {
    provider_config: ProviderConfig,
    // ...
}

let doc = toml_edit::ser::to_document(&app_config).unwrap();
let s = doc.to_string(); // or to_string_in_original_order()

We get a reasonable TOML, but we cannot declaratively say:

  • “serialize provider_config inline”

  • “format this array multiline with one element per line”

  • “insert a comment above provider_config”

  • “place [dex_price_fetcher_config] before [provider_config]”
    …without writing custom traversal and mutation code over DocumentMut.

Workarounds (and why they’re not ideal)

  • Serialize to string and re-parse into DocumentMut, then mutate nodes via inline_table, as_array_mut, etc.
    → Works but is error-prone, verbose, and couples business logic to formatting internals.

  • Custom serde newtypes with manual Serialize impls.
    → Pushes formatting into ad-hoc serialization code and doesn’t scale for mixed inline/multiline requirements across a large schema.

Proposed solutions

Attribute / derive-based formatting hints
Allow users to annotate structs/fields to guide layout. Examples:

#[derive(Serialize)]
struct AppConfig {
    /// Put a comment above this table
    #[toml_edit(comment = "Provider endpoints and options")]
    #[toml_edit(order = 10)]
    provider_config: ProviderConfig,

    /// Emit as an inline table
    #[toml_edit(inline)]
    network: Network,
}

#[derive(Serialize)]
struct ProviderConfig {
    /// Multiline array, one element per line
    #[toml_edit(array = "multiline", per_line = 1, trailing_comma = false)]
    read_only_rpc_urls: Vec<Vec<String>>,

    /// Keep inline (even if long)
    #[toml_edit(inline)]
    write_only_rpc_url: String,
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions