What is the difference between reqwest::Response::json and text for deserializing response bodies?

Response::text() consumes the response body and returns it as a String, automatically decoding the bytes according to the charset specified in the Content-Type header (defaulting to UTF-8 if unspecified). Response::json() consumes the response body and directly deserializes it into any type implementing serde::Deserialize, returning an error if the body isn't valid JSON or doesn't match the target type. The key distinction is that text() gives you the raw string representation for further processing, while json() performs immediate deserialization—json() is essentially text() followed by serde_json::from_str(), but with optimized memory handling since it can deserialize directly from the response body bytes without an intermediate string allocation when the charset is UTF-8.

Basic Usage Comparison

use reqwest::Error;
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct User {
    id: u64,
    name: String,
    email: String,
}
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users/1").await?;
    
    // Using text() - get raw string
    let text = response.text().await?;
    println!("Raw text: {}", text);
    // You would then parse manually if needed:
    // let user: User = serde_json::from_str(&text)?;
    
    // Using json() - deserialize directly
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users/1").await?;
    let user: User = response.json().await?;
    println!("User: {:?}", user);
    
    Ok(())
}

text() returns the raw body as a string; json() deserializes directly into your type.

Understanding What Each Method Does

use reqwest::Error;
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct Post {
    id: u32,
    title: String,
    body: String,
}
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    // text() returns the body as a String
    let response = reqwest::get("https://jsonplaceholder.typicode.com/posts/1").await?;
    let text: String = response.text().await?;
    println!("text() returns: {}", text.len());
    
    // The text is the raw JSON string
    println!("First 100 chars: {}...", &text[..100.min(text.len())]);
    
    // json() deserializes directly into your type
    let response = reqwest::get("https://jsonplaceholder.typicode.com/posts/1").await?;
    let post: Post = response.json().await?;
    println!("\njson() returns: {:?}", post);
    
    // The response body is consumed and deserialized in one step
    
    Ok(())
}

text() gives you the raw string for any processing; json() handles deserialization for you.

Error Handling Differences

use reqwest::Error;
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct User {
    id: u64,
    name: String,
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // json() can fail in multiple ways:
    // 1. Network error (same as text())
    // 2. Invalid JSON syntax
    // 3. JSON doesn't match the target type
    
    // Example: JSON that doesn't match the struct
    let response = reqwest::get("https://jsonplaceholder.typicode.com/posts/1").await?;
    
    // This will fail because the response has "title" and "body",
    // but our User struct expects "name"
    let result: Result<User, _> = response.json().await;
    match result {
        Ok(user) => println!("User: {:?}", user),
        Err(e) => {
            // Error::decode means JSON parsing failed
            // or the structure didn't match
            println!("Deserialization error: {}", e);
            if e.is_decode() {
                println!("  - Invalid JSON or type mismatch");
            }
        }
    }
    
    // With text(), you get the raw text and can handle parsing yourself
    let response = reqwest::get("https://jsonplaceholder.typicode.com/posts/1").await?;
    let text = response.text().await?;
    
    // You can inspect the text before parsing
    println!("Got text, attempting manual parse...");
    match serde_json::from_str::<User>(&text) {
        Ok(user) => println!("User: {:?}", user),
        Err(e) => {
            // You still have the original text available
            println!("Parse error: {}", e);
            println!("Original text was: {}...", &text[..50.min(text.len())]);
        }
    }
    
    Ok(())
}

json() combines network and deserialization errors; text() lets you separate concerns.

When to Use text()

use reqwest::Error;
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct ApiResponse<T> {
    data: T,
    meta: Option<serde_json::Value>,
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // USE CASE 1: Response might not be JSON
    
    let response = reqwest::get("https://example.com").await?;
    let text = response.text().await?;
    // HTML or other non-JSON content
    if text.starts_with("<!DOCTYPE") || text.starts_with("<html") {
        println!("Got HTML response");
    }
    
    // USE CASE 2: Need to inspect before parsing
    
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users/1").await?;
    let text = response.text().await?;
    
    // Check content before parsing
    if text.is_empty() {
        println!("Empty response");
    } else if text.starts_with('{') {
        let user: serde_json::Value = serde_json::from_str(&text)?;
        println!("JSON object: {}", user);
    } else if text.starts_with('[') {
        let users: Vec<serde_json::Value> = serde_json::from_str(&text)?;
        println!("JSON array with {} items", users.len());
    }
    
    // USE CASE 3: Multiple parsing attempts
    
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users/1").await?;
    let text = response.text().await?;
    
    // Try parsing as different types
    #[derive(Debug, Deserialize)]
    struct UserV1 { id: u32, name: String }
    #[derive(Debug, Deserialize)]
    struct UserV2 { id: u32, name: String, email: String }
    
    if let Ok(user) = serde_json::from_str::<UserV2>(&text) {
        println!("Parsed as V2: {:?}", user);
    } else if let Ok(user) = serde_json::from_str::<UserV1>(&text) {
        println!("Parsed as V1: {:?}", user);
    }
    
    // USE CASE 4: Logging and debugging
    
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users/1").await?;
    let text = response.text().await?;
    println!("Raw response for debugging: {}", text);
    let user: serde_json::Value = serde_json::from_str(&text)?;
    
    Ok(())
}

Use text() when you need raw access, multiple parse attempts, or non-JSON content.

When to Use json()

use reqwest::Error;
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct User {
    id: u64,
    name: String,
    email: String,
}
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    // USE CASE 1: Known JSON structure, direct deserialization
    
    let user: User = reqwest::get("https://jsonplaceholder.typicode.com/users/1")
        .await?
        .json()
        .await?;
    println!("User: {:?}", user);
    
    // USE CASE 2: Collection of items
    
    let users: Vec<User> = reqwest::get("https://jsonplaceholder.typicode.com/users")
        .await?
        .json()
        .await?;
    println!("Got {} users", users.len());
    
    // USE CASE 3: Nested structures
    
    #[derive(Debug, Deserialize)]
    struct Post {
        id: u32,
        title: String,
        user: User,
    }
    
    // USE CASE 4: Generic API responses
    
    #[derive(Debug, Deserialize)]
    struct ApiResponse<T> {
        data: T,
        status: String,
    }
    
    // Works with generic types
    let response: ApiResponse<User> = reqwest::get("https://jsonplaceholder.typicode.com/users/1")
        .await?
        .json()
        .await?;
    
    // USE CASE 5: Simple, clean code
    
    // One-liner for simple cases
    let user: User = reqwest::get("https://jsonplaceholder.typicode.com/users/1")
        .await?
        .json()
        .await?;
    
    Ok(())
}

Use json() when you know the structure and want clean, direct deserialization.

Charset Handling with text()

use reqwest::Error;
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    // text() automatically handles charset decoding
    // based on Content-Type header
    
    // If Content-Type: text/plain; charset=utf-8
    // -> Decodes as UTF-8
    
    // If Content-Type: text/plain; charset=iso-8859-1
    // -> Decodes as ISO-8859-1
    
    // If no charset specified
    // -> Defaults to UTF-8
    
    // If Content-Type: application/json
    // -> Decodes as UTF-8 (JSON is always UTF-8)
    
    let response = reqwest::get("https://example.com").await?;
    
    // Check the content type before getting text
    let content_type = response.headers()
        .get(reqwest::header::CONTENT_TYPE)
        .and_then(|v| v.to_str().ok())
        .unwrap_or("unknown");
    
    println!("Content-Type: {}", content_type);
    
    let text = response.text().await?;
    println!("Decoded text length: {}", text.len());
    
    // json() doesn't do charset conversion
    // JSON is defined as UTF-8, so json() expects UTF-8 bytes
    // Non-UTF-8 responses will cause json() to fail
    
    Ok(())
}

text() handles charset decoding; json() assumes UTF-8 as per JSON specification.

Memory and Performance Considerations

use reqwest::Error;
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct LargeResponse {
    items: Vec<Item>,
}
 
#[derive(Debug, Deserialize)]
struct Item {
    id: u64,
    name: String,
    description: String,
}
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    // json() can be more memory efficient
    // It can deserialize directly from the response body bytes
    // without creating an intermediate String
    
    // Conceptually:
    // json() = serde_json::from_slice(&response_bytes)
    // text() = String::from_utf8(response_bytes) then serde_json::from_str(&text)
    
    // For large responses, json() avoids the intermediate string
    
    // But if you need both the raw text AND the parsed value:
    // Use text() and parse separately
    
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users").await?;
    
    // If you need both:
    // Option A: text() then parse
    let text = response.text().await?;
    let users: Vec<serde_json::Value> = serde_json::from_str(&text)?;
    // Now you have both 'text' and 'users'
    
    // Option B: json() with Value type
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users").await?;
    let value: serde_json::Value = response.json().await?;
    // You can serialize Value back to string if needed
    let text = serde_json::to_string(&value)?;
    
    // Option A is usually more efficient if you need the raw text
    
    Ok(())
}

json() can deserialize directly from bytes; text() creates an intermediate string.

Handling Errors Gracefully

use reqwest::{Error, Response, StatusCode};
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct User {
    id: u64,
    name: String,
}
 
#[derive(Debug, Deserialize)]
struct ErrorResponse {
    message: String,
    code: u32,
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users/9999").await?;
    
    // Check status before deserializing
    let status = response.status();
    
    if status.is_success() {
        let user: User = response.json().await?;
        println!("User: {:?}", user);
    } else {
        // For error responses, structure might be different
        // Use text() to inspect or generic Value type
        let error_text = response.text().await?;
        println!("Error response ({}): {}", status, error_text);
        
        // Try to parse as error response
        if let Ok(error) = serde_json::from_str::<ErrorResponse>(&error_text) {
            println!("Error code: {}, message: {}", error.code, error.message);
        }
    }
    
    // Alternative: Use a custom error handling approach
    async fn parse_response<T: for<'de> Deserialize<'de>>(
        response: Response
    ) -> Result<T, Box<dyn std::error::Error>> {
        let status = response.status();
        let text = response.text().await?;
        
        if status.is_success() {
            Ok(serde_json::from_str(&text)?)
        } else {
            Err(format!("HTTP {}: {}", status, text).into())
        }
    }
    
    Ok(())
}

Check HTTP status before deserializing; use text() for error responses with unknown structure.

Working with Different Content Types

use reqwest::Error;
use serde::Deserialize;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::get("https://example.com/api").await?;
    
    // Check content type to decide how to handle response
    let content_type = response.headers()
        .get(reqwest::header::CONTENT_TYPE)
        .and_then(|v| v.to_str().ok())
        .unwrap_or("");
    
    if content_type.contains("application/json") {
        // Use json() for JSON content
        let data: serde_json::Value = response.json().await?;
        println!("JSON: {}", data);
    } else if content_type.contains("text/") {
        // Use text() for text content
        let text = response.text().await?;
        println!("Text: {}", text);
    } else if content_type.contains("application/octet-stream") {
        // Use bytes() for binary content
        let bytes = response.bytes().await?;
        println!("Binary: {} bytes", bytes.len());
    } else {
        // Default to text for unknown types
        let text = response.text().await?;
        println!("Unknown content type: {}", content_type);
        println!("Body: {}", text);
    }
    
    Ok(())
}

Check Content-Type header to choose between json(), text(), or bytes().

Combining Both Approaches

use reqwest::Error;
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct User {
    id: u64,
    name: String,
}
 
#[derive(Debug)]
enum ParseError {
    Reqwest(Error),
    Json(serde_json::Error),
}
 
impl From<Error> for ParseError {
    fn from(e: Error) -> Self {
        ParseError::Reqwest(e)
    }
}
 
impl From<serde_json::Error> for ParseError {
    fn from(e: serde_json::Error) -> Self {
        ParseError::Json(e)
    }
}
 
#[tokio::main]
async fn main() -> Result<(), ParseError> {
    // Strategy: Use text() for debugging, then parse
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users/1").await?;
    
    let text = response.text().await?;
    
    // Log for debugging
    println!("Response body: {}", text);
    
    // Parse with better error context
    let user: User = serde_json::from_str(&text).map_err(|e| {
        println!("Failed to parse JSON: {}", e);
        println!("Original response: {}", text);
        e
    })?;
    
    println!("User: {:?}", user);
    
    // Alternative: Try json() first, fall back to text() on error
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users/1").await?;
    
    // This doesn't work because json() consumes the response
    // You can't fall back to text() after json() fails
    
    // Solution: Use bytes() to get raw bytes, then try both
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users/1").await?;
    let bytes = response.bytes().await?;
    
    // Try JSON parse first
    match serde_json::from_slice::<User>(&bytes) {
        Ok(user) => println!("Parsed as JSON: {:?}", user),
        Err(_) => {
            // Fall back to text
            let text = String::from_utf8_lossy(&bytes);
            println!("Not valid JSON User: {}", text);
        }
    }
    
    Ok(())
}

Use bytes() when you need to try multiple approaches with the same response body.

Response Body Consumption

use reqwest::Error;
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct User {
    id: u64,
    name: String,
}
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    // IMPORTANT: Response body can only be consumed once
    
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users/1").await?;
    
    // This consumes the response body
    let user: User = response.json().await?;
    
    // This would panic or return an error:
    // let text = response.text().await?;  // ERROR: body already consumed
    
    // Similarly:
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users/1").await?;
    let text = response.text().await?;
    
    // This would fail:
    // let user: User = response.json().await?;  // ERROR: body already consumed
    
    // If you need both, use bytes() and process manually
    let response = reqwest::get("https://jsonplaceholder.typicode.com/users/1").await?;
    let bytes = response.bytes().await?;
    
    // Now you have the bytes, can process multiple times
    let text = String::from_utf8_lossy(&bytes);
    println!("Text: {}", text);
    
    let user: User = serde_json::from_slice(&bytes)?;
    println!("User: {:?}", user);
    
    Ok(())
}

Response body is consumed by text(), json(), or bytes(); you can only call one.

Synthesis

Method comparison:

Method Returns Use Case
text() String Raw string access, non-JSON content, debugging
json() T: Deserialize Direct deserialization, known JSON structure
bytes() Bytes Binary data, multiple processing attempts

When to use each method:

Scenario Recommended Method
Known JSON structure json()
Need raw text for logging text()
Non-JSON response (HTML, plain text) text()
Binary data (images, files) bytes()
Unknown structure, might not be JSON text()
Multiple parse attempts text() or bytes()
Debugging deserialization errors text() + manual parse
Large JSON responses json() (avoids intermediate string)
Need both raw and parsed bytes() + from_slice

Error types:

Method Error Sources
text() Network error, charset decoding error
json() Network error, charset error, JSON parse error, type mismatch

Key insight: The choice between json() and text() is about separation of concerns versus convenience. json() combines HTTP response handling with deserialization—convenient when you know the structure and trust the server. text() gives you control by separating the steps: first get the raw response, then decide how to parse it. This separation is valuable when handling potentially malformed responses, varying content types, or when you need to log raw responses for debugging. Internally, json() is optimized to deserialize directly from the response body bytes when the charset is UTF-8, avoiding an intermediate string allocation—this makes it more efficient than text() followed by manual parsing for the common case of UTF-8 JSON. However, if you need both the raw text and the parsed value, using text() once is more efficient than calling json() and re-serializing the result. For maximum flexibility with large responses, bytes() gives you the raw bytes which you can then convert to text or parse as JSON as needed.