---
title: How to Parse TOML in Rust
summary: Parse TOML in Rust by using the toml crate with serde to deserialize text into structs.
lang: en
slug: how-to-parse-toml-in-rust
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-parse-toml-in-rust/
---

# How to Parse TOML in Rust



## When text needs to become data

You are building a command-line tool that needs configuration. You write a `config.toml` file with your settings. Now your Rust program needs to read that file and turn the text into actual data you can use. You could write a parser from scratch, but that is reinventing the wheel. The ecosystem has a standard way to handle this that feels natural once you see the pattern.

Rust makes configuration parsing safe and ergonomic. You define the shape of your data, and the compiler guarantees the data fits.

## The serde engine

Parsing TOML in Rust relies on a pair of crates: `toml` and `serde`. The `toml` crate knows how to read the text format. `serde` is the workhorse that turns that text into Rust types.

Think of `serde` like a universal adapter for data. You define the shape of your data using a Rust struct. `serde` looks at that shape, reads the TOML, and fills in the blanks. If the TOML matches your struct, you get a populated object. If it does not, you get a clear error.

The magic happens through the `Deserialize` trait. When you add `#[derive(Deserialize)]` to a struct, the compiler generates code that tells `serde` how to construct an instance of that struct from external data. The `toml` crate provides the deserializer that feeds data into this generated code.

Add these dependencies to your `Cargo.toml`:

```toml
[dependencies]
toml = "0.8"
serde = { version = "1.0", features = ["derive"] }
```

The `derive` feature in `serde` is essential. It enables the `#[derive(Deserialize)]` macro. Without it, you cannot use the derive attribute.

## Minimal parsing example

Start with a simple struct and a string literal. This example shows the core pattern without file I/O noise.

```rust
use serde::Deserialize;

/// Configuration for the application.
#[derive(Deserialize, Debug)]
struct AppConfig {
    /// The name of the app.
    name: String,
    /// The version number.
    version: u32,
}

fn main() {
    let toml_text = r#"
        name = "my-tool"
        version = 2
    "#;

    // Parse the string into our struct.
    let config: AppConfig = toml::from_str(toml_text).expect("Failed to parse config");

    println!("Running {} v{}", config.name, config.version);
}
```

The `r#` syntax creates a raw string literal. This lets you include quotes and newlines without escaping. The `toml::from_str` function takes the string and returns a `Result`. The `expect` call panics if parsing fails, which is fine for a quick test but not for production code.

Derive the trait. Parse the string. Trust the types.

## How the parser fills the struct

When you call `toml::from_str`, the `toml` crate parses the text into an intermediate representation. Then `serde` takes over. It uses the `Deserialize` trait on `AppConfig` to map the TOML keys to struct fields.

The `name` field expects a `String`, so `serde` takes the TOML string and creates a Rust `String`. The `version` field expects a `u32`, so `serde` parses the integer. If everything matches, you get your struct.

The mapping is based on field names. By default, `serde` expects the TOML keys to match the Rust field names exactly. Rust uses `snake_case`. TOML often uses `kebab-case`. This mismatch causes a common error.

Add `#[serde(rename_all = "kebab-case")]` to the struct to handle this automatically. This is a community convention for configuration structs. It lets you write `my-tool` in the TOML file while keeping `my_tool` in the Rust code.

```rust
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct AppConfig {
    app_name: String,
    log_level: String,
}
```

Now the TOML key `app-name` maps to the field `app_name`. The rename attribute applies to all fields. Use `#[serde(rename = "custom-key")]` on a specific field if you need a one-off exception.

Convention aside: Always add `#[derive(Debug)]` to config structs. It lets you print the struct during development. You will thank yourself when debugging a weird config value.

## Real-world configuration loading

Real applications read config from files. This requires file I/O and proper error handling. The `Result` type is your friend here.

```rust
use serde::Deserialize;
use std::fs;

/// Database connection settings.
#[derive(Deserialize, Debug)]
struct DatabaseConfig {
    host: String,
    port: u16,
}

/// Top-level configuration.
#[derive(Deserialize, Debug)]
struct Config {
    app_name: String,
    debug: bool,
    database: DatabaseConfig,
    /// Optional feature flags.
    features: Option<Vec<String>>,
}

/// Load configuration from a TOML file.
fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
    // Read the file contents into a String.
    let content = fs::read_to_string(path)?;

    // Parse the TOML string into the Config struct.
    let config: Config = toml::from_str(&content)?;

    Ok(config)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = load_config("config.toml")?;
    println!("{:#?}", config);
    Ok(())
}
```

The `load_config` function returns a `Result`. The `?` operator propagates errors. If the file is missing, `read_to_string` returns an error and `load_config` returns early. If the TOML is malformed, `from_str` returns an error and `load_config` returns early.

The `main` function also returns a `Result`. This lets you use `?` in `main` and have the runtime print the error for you. This is standard practice for CLI tools.

Nested structs work naturally. `serde` recurses into the `database` field and deserializes the `DatabaseConfig` struct. The `features` field is an `Option<Vec<String>>`. If the key is missing in the TOML, `serde` sets the field to `None`. If the key is present, `serde` parses the array.

Return `Result`. Let the caller decide how to handle a bad config file.

## Handling missing and optional fields

Configuration files often have defaults. You do not want to require every field in the TOML if a sensible default exists.

Use `Option<T>` for fields that might be missing and where `None` is a valid state. Use `#[serde(default)]` for fields that should fall back to a default value when missing.

```rust
#[derive(Deserialize, Debug)]
struct ServerConfig {
    host: String,
    /// Defaults to 8080 if not specified.
    #[serde(default = "default_port")]
    port: u16,
    /// Defaults to false if not specified.
    #[serde(default)]
    debug: bool,
}

fn default_port() -> u16 {
    8080
}
```

The `#[serde(default)]` attribute on `debug` uses the `Default` trait for `bool`, which is `false`. The `#[serde(default = "default_port")]` attribute calls the `default_port` function. This function must be in scope.

You can also use `#[serde(default)]` on a struct field that implements `Default`. For custom types, implement `Default` on the type itself.

If you want to reject unknown fields, add `#[serde(deny_unknown_fields)]`. This turns typos in the TOML file into errors. Without this, `serde` silently ignores extra keys. This is a safety feature for configuration.

```rust
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct StrictConfig {
    name: String,
}
```

If the TOML contains `name = "test"` and `typo_field = "oops"`, the parse fails with an error about an unknown field.

Use `Option` for truly optional data. Use `default` for sensible fallbacks. Deny unknown fields to catch typos early.

## Pitfalls and compiler errors

Parsing TOML trips up on a few common patterns. The compiler and runtime errors point you in the right direction.

If you forget `#[derive(Deserialize)]`, the compiler rejects the code with `E0277` (the trait `Deserialize` is not implemented for `Config`). The fix is to add the derive attribute.

```rust
// This fails with E0277.
struct Config {
    name: String,
}

let config: Config = toml::from_str(toml_str)?;
```

If you have a type mismatch, `serde` returns a runtime error. For example, if the TOML has `version = "two"` but the struct expects `u32`, the parse fails. The error message tells you the expected type and the actual value.

If you try to use `toml::from_str` with a type that does not implement `Deserialize`, you get `E0277`. This also happens if you forget to add the `derive` feature to `serde` in `Cargo.toml`.

Case sensitivity causes silent failures if you are not careful. If the struct uses `snake_case` and the TOML uses `kebab-case`, the fields end up as default values or `None`. The parse succeeds, but the data is wrong. Use `#[serde(rename_all = "kebab-case")]` to prevent this.

If you hardcode a wrong type in the struct, the compiler catches it with `E0308` (mismatched types). For example, if you declare `version: String` but try to assign a `u32` literal, the compiler rejects it. This is a compile-time check, not a parsing error.

Check for `E0277` first. The derive is usually the missing piece.

## Decision matrix: parsing strategies

Choose the parsing function based on your data source and performance needs.

Use `toml::from_str` when you have the TOML content as a `String` or `&str`. This is the standard choice for most applications.

Use `toml::from_slice` when you read a file into a `Vec<u8>` and want to avoid an extra allocation for the string. This is slightly more efficient for large files.

Use `toml::Value` when you need to inspect the structure dynamically without a predefined struct. This is useful for tools that process arbitrary TOML files.

Use `serde_json` when your config format is JSON instead of TOML. The API is identical. You just swap the crate.

Reach for manual parsing only when you are building a custom DSL and standard TOML is too restrictive. This is rare. The `toml` crate covers almost all use cases.

Match the parsing function to your data source. Avoid unnecessary allocations.

## Where to go next

- [How to Use Serde with TOML Files in Rust](/en/how-to-use-serde-with-toml-files-in-rust/)
- [How to use toml crate in Rust TOML parser](/en/how-to-use-toml-crate-in-rust-toml-parser/)
- [How to parse TOML](/en/how-to-parse-toml/)