---
title: How to Convert Between Data Formats in Rust
summary: Convert data between Rust structs and formats like JSON using the serde crate and its derive macros.
lang: en
slug: how-to-convert-between-data-formats-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-convert-between-data-formats-in-rust/
---

# How to Convert Between Data Formats in Rust



## The missing JSON button

You come from JavaScript. You have an object. You call `JSON.stringify(obj)` and get a string. You switch to Rust. You have a struct. You look for a `to_json` method. It's not there. The standard library doesn't know what JSON is. Rust treats data formats as external concerns. The core language focuses on memory safety and performance, not on parsing text protocols.

This separation is intentional. Rust avoids baking in opinions about data formats. Instead, the ecosystem converged on a single, powerful abstraction. You don't write code to serialize JSON. You write code to describe your data. Then you plug in a crate that knows how to turn that description into JSON, YAML, TOML, or binary formats.

The crate is `serde`. The name stands for "serialization framework." It defines the traits. Other crates implement the formats. Your structs implement the traits once. You can swap formats without touching your data model. This is the "write once, serialize anywhere" pattern.

## serde is the translator, not the format

`serde` provides two core traits: `Serialize` and `Deserialize`. A type that implements `Serialize` can be turned into a stream of data. A type that implements `Deserialize` can be reconstructed from a stream. Most of the time, you want both.

You rarely implement these traits by hand. `serde` provides a procedural macro that generates the implementations for you. You add `#[derive(Serialize, Deserialize)]` to your struct, and the compiler writes the code. The macro inspects your fields, checks their types, and emits trait implementations that call into the format crate.

This approach has a major advantage. The code generation happens at compile time. There is no reflection. There is no runtime type introspection. The generated code is as fast as if you had written the serialization logic manually. You get flexibility without paying a performance penalty.

```rust
use serde::{Serialize, Deserialize};

/// Configuration for a network service.
#[derive(Serialize, Deserialize)]
struct ServiceConfig {
    host: String,
    port: u16,
    debug: bool,
}

fn main() {
    let config = ServiceConfig {
        host: "localhost".to_string(),
        port: 8080,
        debug: true,
    };

    // Serialize to a JSON string.
    let json = serde_json::to_string(&config).unwrap();
    println!("Serialized: {}", json);

    // Deserialize back to the struct.
    let parsed: ServiceConfig = serde_json::from_str(&json).unwrap();
    println!("Parsed port: {}", parsed.port);
}
```

The `#[derive(...)]` attribute triggers the macro. The macro generates `impl Serialize for ServiceConfig` and `impl Deserialize for ServiceConfig`. The `serde_json::to_string` function takes any type that implements `Serialize`. The `serde_json::from_str` function returns any type that implements `Deserialize`. The format crate bridges your type to the JSON syntax.

Check `Cargo.toml` for the `derive` feature before you question reality. The macro is optional. If you add `serde` to dependencies without the feature, the derive macro won't exist. The compiler will complain that the attribute is unknown.

## Real code handles errors and naming

The example above uses `unwrap()`. That's fine for a quick test. Real code handles errors. Serialization and deserialization can fail. The input string might be malformed. The types might not match. The `serde_json` functions return `Result` types. You need to handle the `Err` case.

Web APIs often use `camelCase` for field names. Rust uses `snake_case`. If you derive serialization directly, your JSON will have `snake_case` keys. The API might reject it. You can fix this with attributes. `serde` provides a rich set of attributes to customize serialization without writing manual code.

```rust
use serde::{Serialize, Deserialize};

/// User data from an external API.
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct ApiUser {
    user_id: u64,
    email_address: String,
    is_active: bool,
    /// Fields marked with skip_serializing_if are omitted when the predicate is true.
    #[serde(skip_serializing_if = "Option::is_none")]
    display_name: Option<String>,
}

fn process_user(json_input: &str) -> Result<ApiUser, serde_json::Error> {
    // from_str returns a Result. Use the ? operator to propagate errors.
    let user: ApiUser = serde_json::from_str(json_input)?;
    Ok(user)
}

fn main() {
    let json = r#"{"userId": 123, "emailAddress": "test@example.com", "isActive": true}"#;
    
    match process_user(json) {
        Ok(user) => println!("Got user: {:?}", user),
        Err(e) => eprintln!("Parse failed: {}", e),
    }
}
```

The `#[serde(rename_all = "camelCase")]` attribute applies to all fields. `user_id` becomes `userId`. `email_address` becomes `emailAddress`. This saves you from annotating every field individually. The `#[serde(skip_serializing_if = "Option::is_none")]` attribute omits the field when the value is `None`. This keeps the output clean and matches common API conventions.

Attributes are your first line of defense against format friction. Write manual impls only when attributes run out of steam. The attribute system covers most real-world needs, including renaming, defaults, skipping, and custom serializers for specific fields.

## When you don't know the shape

Sometimes you receive JSON from an external source and you don't want to define a struct for it. The structure might be dynamic. The schema might change frequently. You can use `serde_json::Value`. This type represents any JSON value. It's an enum with variants for objects, arrays, strings, numbers, booleans, and null.

```rust
use serde_json::Value;

fn check_config(key: &str, json: &str) -> Option<String> {
    let value: Value = serde_json::from_str(json).ok()?;
    
    // Navigate the Value tree safely.
    value.get("settings")
        .and_then(|s| s.get(key))
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
}

fn main() {
    let json = r#"{"settings": {"timeout": 30, "mode": "fast"}}"#;
    
    if let Some(mode) = check_config("mode", json) {
        println!("Mode is {}", mode);
    }
}
```

`Value` is flexible but comes with trade-offs. You lose type safety. The compiler can't check if you're accessing the right keys or types. You get runtime errors instead of compile-time errors. Parsing into `Value` is also slower than parsing into a typed struct because it allocates nodes for the entire tree.

Use `Value` for truly dynamic data or as a temporary measure during prototyping. Don't use it as a crutch for lazy design. If the shape is stable, define a struct. The type system will catch mismatches before the code runs.

Dynamic data is a smell. Reach for `Value` sparingly and convert to typed structs as soon as possible.

## Pitfalls that trip up beginners

**Missing the derive feature.** This is the most common error. You add `serde` to `Cargo.toml`, add `#[derive(Serialize)]`, and the compiler says the attribute is not found. The `derive` macro is behind a feature flag. You need `serde = { version = "1.0", features = ["derive"] }`. Without the feature, the crate compiles but the macro is missing.

**Borrowing lifetimes.** `serde_json::from_str` borrows the input string. If your struct contains `&str` fields, the deserialized struct borrows from the input. The struct cannot outlive the JSON string. If you need owned data, use `String` fields. The deserializer will allocate new strings. This is a common source of lifetime errors. If you see `E0597` (borrowed value does not live long enough), check if your struct borrows from the input when it should own the data.

**Trait bounds.** If you try to serialize a type that doesn't implement `Serialize`, the compiler rejects the code with `E0277` (trait bound not satisfied). This happens when you have a field of a type that `serde` doesn't know about. You need to derive the traits on that type, or implement them manually, or skip the field.

**Error handling.** `serde_json::Error` implements `std::error::Error`. You can use the `?` operator to propagate errors. The error messages are detailed. They include the line and column where the parse failed, and what was expected. Log these errors. They help debug integration issues.

**Enums.** Enums serialize to JSON objects by default. A variant `Status::Active` becomes `{"Active": null}`. A variant `Status::Pending(String)` becomes `{"Pending": "reason"}`. This is the externally tagged representation. It's explicit and unambiguous. If you need a different format, like a plain string, use `#[serde(untagged)]` or `#[serde(tag = "...")]`. Untagged enums can be ambiguous during deserialization. The deserializer tries variants in order and stops at the first match. This can lead to subtle bugs if variants overlap.

Treat `serde` errors as data validation failures, not just parsing bugs. The error message tells you exactly where the contract broke.

## Decision matrix

Use `#[derive(Serialize, Deserialize)]` for standard structs and enums where the data shape matches the format. This is the default for 99% of use cases. The macro generates efficient, correct code with zero boilerplate.

Use `serde_json::Value` when the input structure is dynamic or unknown at compile time, accepting the trade-off of losing type safety and incurring higher allocation overhead.

Use `#[serde(rename_all = "...")]` when the external format uses a different naming convention than Rust's `snake_case`. Apply this at the struct level to avoid annotating every field.

Use manual `impl Serialize` only when attribute-based customization cannot express the required logic, such as complex conditional serialization based on internal state that spans multiple fields.

Reach for `serde` with a specific format crate like `serde_yaml` or `serde_cbor` when you need to switch formats without rewriting your data model. Your structs stay the same. You only change the serialization function calls.

## Where to go next

- [How to deserialize JSON to structs](/en/how-to-deserialize-json-to-structs/)
- [How to skip fields in serde serialization](/en/how-to-skip-fields-in-serde-serialization/)
- [How to handle dates with serde](/en/how-to-handle-dates-with-serde/)