DevToolBoxGRÁTIS
Blog

JSON para Struct Rust: Guia Completo com serde_json e serde Derive

11 min de leituraby DevToolBox

JSON para Rust Struct: Guia Completo com Serde

Converta JSON em structs Rust com macros derive Serde, atributos personalizados e desserialização com segurança de tipos.

TL;DR

Add serde = { version: "1", features: ["derive"] } and serde_json = "1" to Cargo.toml. Annotate your structs with #[derive(Serialize, Deserialize)]. Use #[serde(rename_all = "camelCase")] for JavaScript APIs, Option<T> for nullable fields, and serde_json::Value for fully dynamic JSON.

1. Why Use Serde for JSON in Rust

Rust has several JSON libraries, but Serde with serde_json is the ecosystem standard. Here is how the main options compare:

CratePerformanceEase of UseFeaturesEcosystem Support
serde_jsonVery fastExcellent (derive)Full: rename, skip, flatten, tagDominant standard
json crateModerateGood (no derive)Basic value parsingMinimal, niche use
simd-jsonFastest (SIMD)Good (serde compat)Mostly serde_json APIGrowing, x86/x64 only

For most Rust projects, serde_json is the right choice. It integrates with the entire Rust ecosystem — Axum, Actix-web, Reqwest, SQLx, and hundreds of other crates all use Serde as their serialization layer.

2. Basic Serde Setup

Start by adding the dependencies to your Cargo.toml:

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

The features = ["derive"] flag enables the procedural macros. Now define a struct and derive both traits:

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: u64,
    name: String,
    email: String,
    age: u32,
    active: bool,
}

fn main() -> Result<(), serde_json::Error> {
    // Deserialize: JSON string -> Rust struct
    let json = r#"{
        "id": 1,
        "name": "Alice",
        "email": "alice@example.com",
        "age": 30,
        "active": true
    }"#;

    let user: User = serde_json::from_str(json)?;
    println!("Name: {}", user.name);

    // Serialize: Rust struct -> JSON string
    let json_out = serde_json::to_string_pretty(&user)?;
    println!("{}", json_out);

    // from_slice works on bytes instead of &str
    let bytes = json.as_bytes();
    let user2: User = serde_json::from_slice(bytes)?;
    println!("Age: {}", user2.age);

    Ok(())
}

3. Field Renaming and Attributes

Serde provides a rich set of field-level and struct-level attributes for controlling serialization behavior:

use serde::{Deserialize, Serialize};

// rename_all: convert all fields to camelCase
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiResponse {
    user_id: u64,       // JSON key: "userId"
    first_name: String, // JSON key: "firstName"
    last_name: String,  // JSON key: "lastName"
    created_at: String, // JSON key: "createdAt"
}

#[derive(Debug, Serialize, Deserialize)]
struct Product {
    // Rename a single field
    #[serde(rename = "product_id")]
    id: u64,

    // Skip this field entirely (not serialized or deserialized)
    #[serde(skip)]
    internal_cache: Option<String>,

    // Use Default::default() when field is absent from JSON
    #[serde(default)]
    discount: f64,

    // Skip serializing if the value is None
    #[serde(skip_serializing_if = "Option::is_none")]
    coupon_code: Option<String>,

    // Skip serializing if condition is met
    #[serde(skip_serializing_if = "Vec::is_empty")]
    tags: Vec<String>,
}

// Supported rename_all strategies:
// "camelCase"          → userId
// "PascalCase"         → UserId
// "snake_case"         → user_id
// "SCREAMING_SNAKE_CASE" → USER_ID
// "kebab-case"         → user-id
// "SCREAMING-KEBAB-CASE" → USER-ID

4. Optional and Default Fields

Handling nullable and missing fields is one of the most common JSON parsing challenges. Serde provides several tools:

use serde::{Deserialize, Serialize};

fn default_page_size() -> usize { 20 }
fn default_active() -> bool { true }

#[derive(Debug, Serialize, Deserialize)]
struct UserProfile {
    id: u64,
    name: String,

    // Nullable: field can be null or a string
    // None serializes as JSON null
    avatar_url: Option<String>,

    // Missing field → uses Default::default() → 0
    #[serde(default)]
    login_count: u32,

    // Missing field → calls default_page_size() → 20
    #[serde(default = "default_page_size")]
    page_size: usize,

    // Missing or null → true
    #[serde(default = "default_active")]
    is_active: bool,

    // Omit from serialized output when None
    #[serde(skip_serializing_if = "Option::is_none")]
    bio: Option<String>,
}

// Difference: null vs absent
// Option<T>: field present as null → Some? NO → None
//            field absent          → None
//            field present as value → Some(value)
//
// For truly three-state (absent / null / value):
#[derive(Debug, Serialize, Deserialize)]
struct Patch {
    // None = absent (not updated), Some(None) = set to null, Some(Some(v)) = set value
    #[serde(default, skip_serializing_if = "Option::is_none")]
    email: Option<Option<String>>,
}

5. Nested Structs and Arrays

Serde handles nested structures and arrays automatically through Rust's type system:

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Debug, Serialize, Deserialize)]
struct Address {
    street: String,
    city: String,
    country: String,
    zip_code: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct Tag {
    id: u32,
    name: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct BlogPost {
    id: u64,
    title: String,
    author: Author,               // nested struct
    tags: Vec<Tag>,               // array of structs
    images: Vec<String>,          // array of strings
    metadata: Option<PostMeta>,   // optional nested
    // HashMap for dynamic keys: {"en": "Hello", "es": "Hola"}
    translations: HashMap<String, String>,
}

#[derive(Debug, Serialize, Deserialize)]
struct Author {
    id: u64,
    name: String,
    address: Option<Address>,
}

#[derive(Debug, Serialize, Deserialize)]
struct PostMeta {
    views: u64,
    likes: u32,
}

// Nested arrays
#[derive(Debug, Serialize, Deserialize)]
struct Matrix {
    data: Vec<Vec<f64>>,           // [[1.0, 2.0], [3.0, 4.0]]
}

// HashMap with struct values for dynamic objects
#[derive(Debug, Serialize, Deserialize)]
struct Config {
    settings: HashMap<String, serde_json::Value>,  // arbitrary values
}

6. Enums with Serde

Serde supports four enum representation modes, giving you full control over how discriminated unions appear in JSON:

use serde::{Deserialize, Serialize};

// 1. External tagging (default)
// {"Circle": {"radius": 1.0}}
#[derive(Debug, Serialize, Deserialize)]
enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Point,
}

// 2. Internal tagging: {"type": "Login", "user_id": 42}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
enum Event {
    Login { user_id: u64, timestamp: String },
    Logout { user_id: u64 },
    Purchase { user_id: u64, amount: f64 },
}

// 3. Adjacent tagging: {"type": "Email", "data": {...}}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum Notification {
    Email { to: String, subject: String },
    Push { device_token: String, message: String },
}

// 4. Untagged: tries each variant in order
// Useful for union types from external APIs
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum StringOrNumber {
    Text(String),
    Integer(i64),
    Float(f64),
}

// Simple string enums map to JSON strings
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Status {
    Active,    // "active"
    Inactive,  // "inactive"
    Pending,   // "pending"
}

7. Custom Deserialize with deserialize_with

For special types like dates, use deserialize_with and serialize_with to plug in custom functions:

use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde::de::Error;

// Custom date handling with chrono
// Cargo.toml: chrono = { version = "0.4", features = ["serde"] }
use chrono::{DateTime, Utc, NaiveDate};

fn deserialize_date<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
where
    D: Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(D::Error::custom)
}

fn serialize_date<S>(date: &NaiveDate, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    serializer.serialize_str(&date.format("%Y-%m-%d").to_string())
}

#[derive(Debug, Serialize, Deserialize)]
struct Event {
    name: String,

    #[serde(
        deserialize_with = "deserialize_date",
        serialize_with = "serialize_date"
    )]
    date: NaiveDate,

    // chrono's built-in serde support handles RFC 3339
    created_at: DateTime<Utc>,
}

// Implementing Deserialize manually for complex cases
use serde::de::{self, Visitor, MapAccess};
use std::fmt;

struct PointVisitor;

impl<'de> Visitor<'de> for PointVisitor {
    type Value = (f64, f64);

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        formatter.write_str("an array [x, y] or object {x, y}")
    }

    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
    where
        A: serde::de::SeqAccess<'de>,
    {
        let x = seq.next_element()?.ok_or_else(|| de::Error::invalid_length(0, &self))?;
        let y = seq.next_element()?.ok_or_else(|| de::Error::invalid_length(1, &self))?;
        Ok((x, y))
    }
}

8. serde_json::Value for Dynamic JSON

When the JSON structure is unknown at compile time, use serde_json::Value:

use serde_json::{Value, json};

fn process_dynamic_json(json_str: &str) -> Result<(), serde_json::Error> {
    let v: Value = serde_json::from_str(json_str)?;

    // Navigate with bracket indexing (returns &Value::Null if missing)
    let name = &v["user"]["name"];
    println!("{}", name); // "Alice" or null

    // Type-safe access with as_ methods
    if let Some(id) = v["user"]["id"].as_i64() {
        println!("ID: {}", id);
    }
    if let Some(email) = v["user"]["email"].as_str() {
        println!("Email: {}", email);
    }
    if let Some(active) = v["user"]["active"].as_bool() {
        println!("Active: {}", active);
    }

    // Safer access with get()
    if let Some(user) = v.get("user") {
        if let Some(tags) = user.get("tags") {
            if let Some(arr) = tags.as_array() {
                for tag in arr {
                    println!("Tag: {}", tag.as_str().unwrap_or("unknown"));
                }
            }
        }
    }

    // Build JSON with the json! macro
    let response = json!({
        "status": "ok",
        "count": 42,
        "items": ["a", "b", "c"],
        "meta": {
            "page": 1,
            "per_page": 20
        }
    });
    println!("{}", serde_json::to_string_pretty(&response)?);

    Ok(())
}

// Mix: typed struct with a dynamic field
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct ApiResponse {
    status: String,
    code: u32,
    // This field can be any JSON value
    data: Value,
}

9. Error Handling

serde_json returns a serde_json::Error which tells you what went wrong and where:

use serde::{Deserialize, Serialize};
use serde_json::Error;

#[derive(Debug, Deserialize)]
struct Config {
    host: String,
    port: u16,
    debug: bool,
}

fn parse_config(input: &str) -> Result<Config, Error> {
    // from_str: parse &str
    serde_json::from_str(input)
}

fn parse_config_bytes(input: &[u8]) -> Result<Config, Error> {
    // from_slice: parse &[u8] (avoids UTF-8 validation overhead)
    serde_json::from_slice(input)
}

fn parse_config_reader<R: std::io::Read>(reader: R) -> Result<Config, Error> {
    // from_reader: streaming parse (ideal for files/network)
    serde_json::from_reader(reader)
}

fn main() {
    let bad_json = r#"{"host": "localhost", "port": "not-a-number"}"#;

    match serde_json::from_str::<Config>(bad_json) {
        Ok(config) => println!("Parsed: {:?}", config),
        Err(e) => {
            eprintln!("Parse error: {}", e);
            eprintln!("Line: {}, Column: {}", e.line(), e.column());
            // Check error category
            if e.is_data() {
                eprintln!("Type mismatch or constraint violation");
            } else if e.is_syntax() {
                eprintln!("Invalid JSON syntax");
            } else if e.is_eof() {
                eprintln!("Unexpected end of input");
            }
        }
    }

    // Use anyhow or thiserror in production for better error propagation
    // anyhow::Result<Config> = serde_json::from_str(json).context("parsing config")?;
}

10. Performance Tips

Serde is already very fast, but these techniques maximize throughput for performance-critical paths:

// 1. Use from_reader for large files — avoids loading everything into memory
use std::fs::File;
use std::io::BufReader;

fn load_large_json<T: serde::de::DeserializeOwned>(path: &str) -> serde_json::Result<T> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);  // BufReader is crucial for performance
    serde_json::from_reader(reader)
}

// 2. simd-json for maximum throughput (x86/ARM SIMD acceleration)
// Cargo.toml: simd-json = { version = "0.13", features = ["serde_impl"] }
// simd-json requires &mut [u8] (modifies buffer in place)
// use simd_json;
// let mut buffer = json_bytes.to_vec();
// let value: MyStruct = simd_json::from_slice(&mut buffer)?;

// 3. Avoid serde_json::Value when struct type is known
// Value allocates a heap Map<String, Value> — much slower than typed parsing
// BAD:
// let v: Value = serde_json::from_str(json)?;
// let name = v["name"].as_str();
// GOOD:
// let user: User = serde_json::from_str(json)?;
// let name = &user.name;

// 4. Use &str instead of String for zero-copy deserialization
// The lifetime is tied to the input JSON string
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct ZeroCopy<'a> {
    #[serde(borrow)]
    name: &'a str,    // No allocation — borrows from input
    #[serde(borrow)]
    email: &'a str,
}

// 5. Serialize to writer directly instead of String
fn write_json_to_writer<W: std::io::Write>(
    value: &impl serde::Serialize,
    writer: W,
) -> serde_json::Result<()> {
    serde_json::to_writer(writer, value)
    // or serde_json::to_writer_pretty(writer, value)
}

11. Axum and Actix-web Integration

Both major Rust web frameworks use serde_json under the hood, making JSON handling seamless:

// === Axum ===
// Cargo.toml: axum = { version = "0.7", features = ["json"] }
use axum::{
    extract::Json,
    routing::{get, post},
    Router,
};
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize)]
struct CreateUserRequest {
    name: String,
    email: String,
}

#[derive(Debug, Serialize)]
struct UserResponse {
    id: u64,
    name: String,
    email: String,
    created_at: String,
}

// Json extractor deserializes body; Json return serializes response
async fn create_user(
    Json(payload): Json<CreateUserRequest>,
) -> Json<UserResponse> {
    Json(UserResponse {
        id: 1,
        name: payload.name,
        email: payload.email,
        created_at: "2026-02-27T00:00:00Z".to_string(),
    })
}

// Error handling with axum
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};

async fn safe_create_user(
    Json(payload): Json<CreateUserRequest>,
) -> Result<Json<UserResponse>, (StatusCode, String)> {
    if payload.email.is_empty() {
        return Err((StatusCode::BAD_REQUEST, "Email is required".into()));
    }
    Ok(Json(UserResponse {
        id: 42,
        name: payload.name,
        email: payload.email,
        created_at: "2026-02-27T00:00:00Z".to_string(),
    }))
}

// === Actix-web ===
// Cargo.toml: actix-web = "4"
use actix_web::{web, App, HttpResponse, HttpServer, Responder};

async fn actix_create_user(body: web::Json<CreateUserRequest>) -> impl Responder {
    let response = UserResponse {
        id: 1,
        name: body.name.clone(),
        email: body.email.clone(),
        created_at: "2026-02-27T00:00:00Z".to_string(),
    };
    HttpResponse::Created().json(response)
}

// Actix-web: query parameters from JSON-like structs
use actix_web::web::Query;

#[derive(Debug, Deserialize)]
struct PaginationQuery {
    page: Option<u32>,
    per_page: Option<u32>,
}

async fn list_users(query: Query<PaginationQuery>) -> impl Responder {
    let page = query.page.unwrap_or(1);
    let per_page = query.per_page.unwrap_or(20);
    HttpResponse::Ok().json(serde_json::json!({
        "page": page,
        "per_page": per_page,
        "data": []
    }))
}

12. Common Pitfalls

Watch out for these issues when working with serde_json:

use serde::{Deserialize, Serialize};
use std::borrow::Cow;

// PITFALL 1: Ownership vs borrowing — &str in structs requires lifetime
// This will NOT compile without a lifetime:
// struct Bad { name: &str }  // error: missing lifetime specifier

// Use String (owned) for most cases:
#[derive(Debug, Deserialize)]
struct OwnedVersion {
    name: String,  // Always works
}

// Use &str with lifetime for zero-copy (advanced):
#[derive(Debug, Deserialize)]
struct BorrowedVersion<'a> {
    #[serde(borrow)]
    name: &'a str,  // Borrows from input, input must outlive struct
}

// PITFALL 2: Cow<str> — flexible owned or borrowed
#[derive(Debug, Deserialize, Serialize)]
struct FlexibleStruct<'a> {
    #[serde(borrow)]
    name: Cow<'a, str>,  // Borrows if possible, owns if needed
}

// PITFALL 3: Integer overflow from JSON numbers
// JSON has no integer size constraints; Rust types do
// This panics on overflow in debug mode:
#[derive(Debug, Deserialize)]
struct OverflowRisk {
    count: u8,  // Fails if JSON has count: 300
}

// Use larger types or validate:
#[derive(Debug, Deserialize)]
struct SafeCount {
    count: u64,  // Safe for all positive JSON integers
}

// PITFALL 4: Recursive types must use Box<T>
// This will NOT compile (infinite size):
// struct Node { children: Vec<Node> }

// Correct: use Box for recursion
#[derive(Debug, Serialize, Deserialize)]
struct TreeNode {
    value: i32,
    children: Vec<Box<TreeNode>>,  // Box breaks infinite size
}

// PITFALL 5: Missing serde import causes cryptic errors
// Every file using #[derive(Serialize, Deserialize)] needs:
use serde::{Serialize as _Serialize, Deserialize as _Deserialize};
// Or: use serde::*; (less idiomatic but explicit)

// PITFALL 6: f64 precision for large integers
// JSON: {"id": 9007199254740993}
// f64 cannot represent this exactly — use i64 or u64
#[derive(Debug, Deserialize)]
struct Precise {
    id: u64,      // Correct: exact integer
    score: f64,   // OK: floating-point precision is expected
}

Key Takeaways

  • Add serde = { version: "1", features: ["derive"] } and serde_json = "1" to Cargo.toml
  • #[derive(Serialize, Deserialize)] generates all JSON parsing/writing code at compile time
  • #[serde(rename_all = "camelCase")] maps all snake_case Rust fields to camelCase JSON keys
  • Option<T> handles null/missing fields — #[serde(skip_serializing_if = "Option::is_none")] omits None in output
  • Use #[serde(tag = "type")] for internally tagged enums (discriminated unions)
  • serde_json::Value handles fully dynamic JSON at the cost of heap allocations
  • Use Box<T> for recursive struct types and BufReader + from_reader for large files
  • Axum's Json<T> extractor and Actix-web's web::Json<T> both use serde_json transparently

Frequently Asked Questions

Q: What is Serde and why is it used for JSON in Rust?

Serde is Rust's de-facto serialization/deserialization framework. It provides generic Serialize and Deserialize traits. serde_json is the concrete implementation for JSON. Using #[derive(Serialize, Deserialize)] on your structs, Serde generates all the parsing and serialization code at compile time with zero runtime overhead.

Q: How do I add Serde to my Rust project?

Add to Cargo.toml: serde = { version = "1", features = ["derive"] } and serde_json = "1". The "derive" feature enables the #[derive(Serialize, Deserialize)] macros. Then import with: use serde::{Deserialize, Serialize}; in each file that uses it.

Q: How do I handle camelCase JSON keys with snake_case Rust fields?

Use #[serde(rename_all = "camelCase")] on the struct. This converts all field names automatically: user_name becomes "userName", created_at becomes "createdAt". For a single field use #[serde(rename = "specificKey")]. Supported strategies include camelCase, PascalCase, snake_case, SCREAMING_SNAKE_CASE, and kebab-case.

Q: How does Option<T> work with serde_json for nullable fields?

Option<T> maps naturally: None serializes as null, absent keys deserialize as None. Use #[serde(skip_serializing_if = "Option::is_none")] to omit None fields from output. Use #[serde(default)] to treat absent keys as None. For fields that can be both absent and null, use Option<Option<T>>.

Q: How do I deserialize JSON arrays in Rust?

Use Vec<T> where T implements Deserialize. Serde handles it automatically: let items: Vec<Item> = serde_json::from_str(json)?; In a struct field, declare tags: Vec<Tag>. For optional arrays use Option<Vec<T>>. For heterogeneous arrays use Vec<serde_json::Value>.

Q: What are the different enum tagging strategies in Serde?

Serde supports four enum representations: External tagging (default) wraps data in the variant name key. Internal tagging (#[serde(tag = "type")]) adds a discriminant field inside the object. Adjacent tagging (#[serde(tag = "type", content = "data")]) keeps type and data separate. Untagged (#[serde(untagged)]) tries each variant in order — useful for union types.

Q: When should I use serde_json::Value instead of a struct?

Use serde_json::Value when the JSON structure is unknown at compile time, for truly dynamic data, or when you need to inspect/modify JSON before converting. Value has variants: Null, Bool, Number, String, Array(Vec<Value>), Object(Map<String, Value>). Access with v["key"] and cast with v.as_str(), v.as_i64(), v.as_f64().

Q: How do I integrate serde_json with Axum or Actix-web?

In Axum, use the Json extractor: async fn handler(Json(body): Json<MyRequest>) -> Json<MyResponse>. Axum automatically deserializes requests and serializes responses using serde_json. In Actix-web, use web::Json<T>: async fn handler(body: web::Json<MyRequest>) -> impl Responder. Both frameworks return 422 Unprocessable Entity on deserialization failure.

𝕏 Twitterin LinkedIn
Isso foi útil?

Fique atualizado

Receba dicas de dev e novos ferramentas semanalmente.

Sem spam. Cancele a qualquer momento.

Try These Related Tools

RSJSON to Rust Struct{ }JSON FormatterTSJSON to TypeScriptJSON Validator

Related Articles

JSON para Struct Go: O Guia Completo de Conversão para 2026

Aprenda a converter JSON em structs Go com tags json, tipos aninhados, ponteiros nullable e encoding/json.

JSON para TypeScript Online: O guia completo para desenvolvedores

Aprenda a gerar tipos TypeScript a partir de JSON automaticamente. Interface vs type, campos opcionais/nullable, objetos aninhados, tipos union, validacao Zod, tipos API genericos e boas praticas tsconfig.